feat(ems): 添加产线信息显示和视图切换功能
refactor(dashboard): 移除过时的趋势分析组件 - 在仪表盘表格中添加产线信息显示 - 在设备管理页面增加卡片/表格视图切换功能 - 删除不再使用的趋势分析组件
This commit is contained in:
@@ -1,125 +1,696 @@
|
||||
<template>
|
||||
<el-row>
|
||||
<el-col :span="4">
|
||||
<!-- <div style="padding: 10px;">
|
||||
<el-select v-model="energyType" placeholder="请选择能源类型" @change="refresh">
|
||||
<el-option v-for="item in energyTypeList" :key="item.energyTypeId" :label="item.name" :value="item.energyTypeId" />
|
||||
</el-select>
|
||||
</div> -->
|
||||
|
||||
<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="时间范围">
|
||||
<time-range-picker
|
||||
v-model="timeRangeParams"
|
||||
start-key="recordStartDate"
|
||||
end-key="recordEndDate"
|
||||
:default-start-time="defaultStartTime"
|
||||
:default-end-time="defaultEndTime"
|
||||
@quick-select="loadData"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" icon="el-icon-refresh" @click="loadData">刷新</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<!-- 区域选择,默认展开,最高高度为60%的屏幕尺寸 -->
|
||||
<el-tree @node-click="handleNodeClick" :data="locationList" :props="defaultProps" :default-expand-all="true" :style="{ height: 'calc(50vh - 50px)' }"></el-tree>
|
||||
<!-- 统计卡片 -->
|
||||
<div class="stat-cards">
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="6" v-for="(stat, index) in statistics" :key="index">
|
||||
<div class="stat-card" :class="'stat-' + index">
|
||||
<div class="stat-icon">
|
||||
<i :class="stat.icon"></i>
|
||||
</div>
|
||||
<div class="stat-content">
|
||||
<div class="stat-value">{{ stat.value }}</div>
|
||||
<div class="stat-label">{{ stat.label }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
|
||||
<!-- 设备列表 -->
|
||||
<!-- <ul v-if="locationId">
|
||||
<li></li>
|
||||
</ul>
|
||||
<div v-else>
|
||||
<el-empty description="请选择区域"></el-empty>
|
||||
</div> -->
|
||||
</el-col>
|
||||
<!-- 图表区域 -->
|
||||
<div class="charts-section">
|
||||
<el-row :gutter="20">
|
||||
<!-- 能源消耗趋势图 -->
|
||||
<el-col :span="16">
|
||||
<div class="chart-card">
|
||||
<div class="chart-header">
|
||||
<span class="chart-title">能源消耗趋势</span>
|
||||
</div>
|
||||
<div ref="trendChart" class="chart-container"></div>
|
||||
</div>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="20" v-if="showRight">
|
||||
<!-- <el-tabs v-model="activeTab" type="card">
|
||||
<el-tab-pane label="环比概况" name="1"></el-tab-pane>
|
||||
<el-tab-pane label="近期趋势" name="2"></el-tab-pane>
|
||||
<el-tab-pane label="同比分析" name="3"></el-tab-pane>
|
||||
<el-tab-pane label="环比分析" name="4"></el-tab-pane>
|
||||
</el-tabs> -->
|
||||
<el-tabs v-model="energyType" type="card" @tab-click="refresh">
|
||||
<el-tab-pane v-for="item in energyTypeList" :key="item.energyTypeId" :label="item.name" :name="item.energyTypeId"></el-tab-pane>
|
||||
</el-tabs>
|
||||
<!-- 能源类型占比 -->
|
||||
<el-col :span="8">
|
||||
<div class="chart-card">
|
||||
<div class="chart-header">
|
||||
<span class="chart-title">能源类型占比</span>
|
||||
</div>
|
||||
<div ref="energyTypeChart" class="chart-container"></div>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<Overview ref="overview" :unit="energyUnit" :energyName="energyName" :energyType="energyType" :locationId="locationId" :deviceId="deviceId" />
|
||||
<RecentTrend ref="recentTrend" :unit="energyUnit" :energyName="energyName" :energyType="energyType" :locationId="locationId" :deviceId="deviceId" />
|
||||
<YearToYear ref="yearToYear" :unit="energyUnit" :energyName="energyName" :energyType="energyType" :locationId="locationId" :deviceId="deviceId" />
|
||||
<MonthToMonth ref="monthToMonth" :unit="energyUnit" :energyName="energyName" :energyType="energyType" :locationId="locationId" :deviceId="deviceId" />
|
||||
</el-col>
|
||||
<el-col :span="20" v-else>
|
||||
<el-empty description="请选择能源类型和区域"></el-empty>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="20" style="margin-top: 20px">
|
||||
<!-- 产线能耗对比 -->
|
||||
<el-col :span="12">
|
||||
<div class="chart-card">
|
||||
<div class="chart-header">
|
||||
<span class="chart-title">产线能耗对比</span>
|
||||
</div>
|
||||
<div ref="productionLineChart" class="chart-container"></div>
|
||||
</div>
|
||||
</el-col>
|
||||
|
||||
<!-- 设备能耗排行 -->
|
||||
<el-col :span="12">
|
||||
<div class="chart-card">
|
||||
<div class="chart-header">
|
||||
<span class="chart-title">设备能耗排行 TOP 10</span>
|
||||
</div>
|
||||
<div ref="deviceRankingChart" class="chart-container"></div>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<!-- 产线能源类型二维表 -->
|
||||
<el-row :gutter="20" style="margin-top: 20px">
|
||||
<el-col :span="24">
|
||||
<div class="chart-card">
|
||||
<div class="chart-header">
|
||||
<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-column prop="total" label="合计" fixed="right" width="150">
|
||||
<template slot-scope="scope">
|
||||
{{ scope.row.total ? Number(scope.row.total).toFixed(2) : 0 }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { listEnergyType } from "@/api/ems/energyType";
|
||||
import { listLocation } from "@/api/ems/location";
|
||||
|
||||
import Overview from "./panels/Overview.vue";
|
||||
import RecentTrend from "./panels/RecentTrends.vue";
|
||||
import YearToYear from "./panels/YearOnYear.vue";
|
||||
import MonthToMonth from "./panels/MonthToMonth.vue";
|
||||
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'
|
||||
|
||||
export default {
|
||||
name: "Dashboard",
|
||||
name: 'EmsDashboard',
|
||||
dicts: ['sys_lines'],
|
||||
components: {
|
||||
Overview,
|
||||
RecentTrend,
|
||||
YearToYear,
|
||||
MonthToMonth
|
||||
TimeRangePicker
|
||||
},
|
||||
data() {
|
||||
const now = dayjs()
|
||||
return {
|
||||
energyTypeList: [],
|
||||
energyType: '',
|
||||
locationId: '',
|
||||
deviceId: '',
|
||||
locationList: [
|
||||
],
|
||||
defaultProps: {
|
||||
children: 'children',
|
||||
label: 'name'
|
||||
timeRangeParams: {
|
||||
recordStartDate: now.startOf('month').format('YYYY-MM-DD HH:mm:ss'),
|
||||
recordEndDate: now.endOf('month').format('YYYY-MM-DD HH:mm:ss')
|
||||
},
|
||||
activeTab: '1'
|
||||
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: [],
|
||||
meterList: [],
|
||||
energyRecords: [],
|
||||
statistics: [],
|
||||
tableData: [],
|
||||
trendChartType: 'day',
|
||||
trendChart: null,
|
||||
energyTypeChart: null,
|
||||
productionLineChart: null,
|
||||
deviceRankingChart: null,
|
||||
loading: false,
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.getEnergyTypeList();
|
||||
this.getLocationList();
|
||||
mounted() {
|
||||
this.initCharts()
|
||||
this.loadBasicData()
|
||||
window.addEventListener('resize', this.handleResize)
|
||||
},
|
||||
computed: {
|
||||
showRight() {
|
||||
return this.energyType && (this.deviceId || this.locationId);
|
||||
},
|
||||
energyUnit() {
|
||||
return this.energyTypeList.find(item => item.energyTypeId === this.energyType)?.unit;
|
||||
},
|
||||
energyName() {
|
||||
return this.energyTypeList.find(item => item.energyTypeId === this.energyType)?.name;
|
||||
}
|
||||
beforeDestroy() {
|
||||
window.removeEventListener('resize', this.handleResize)
|
||||
if (this.trendChart) this.trendChart.dispose()
|
||||
if (this.energyTypeChart) this.energyTypeChart.dispose()
|
||||
if (this.productionLineChart) this.productionLineChart.dispose()
|
||||
if (this.deviceRankingChart) this.deviceRankingChart.dispose()
|
||||
},
|
||||
methods: {
|
||||
getEnergyTypeList() {
|
||||
listEnergyType({ pageNum: 1, pageSize: 9999 }).then(response => {
|
||||
this.energyTypeList = response.rows;
|
||||
this.energyType = this.energyTypeList[0]?.energyTypeId;
|
||||
});
|
||||
initCharts() {
|
||||
this.trendChart = echarts.init(this.$refs.trendChart)
|
||||
this.energyTypeChart = echarts.init(this.$refs.energyTypeChart)
|
||||
this.productionLineChart = echarts.init(this.$refs.productionLineChart)
|
||||
this.deviceRankingChart = echarts.init(this.$refs.deviceRankingChart)
|
||||
},
|
||||
getLocationList() {
|
||||
listLocation().then(response => {
|
||||
this.locationList = this.handleTree(response.data, "locationId", "parentId");
|
||||
});
|
||||
handleResize() {
|
||||
this.trendChart && this.trendChart.resize()
|
||||
this.energyTypeChart && this.energyTypeChart.resize()
|
||||
this.productionLineChart && this.productionLineChart.resize()
|
||||
this.deviceRankingChart && this.deviceRankingChart.resize()
|
||||
},
|
||||
handleNodeClick(data) {
|
||||
this.locationId = data.locationId;
|
||||
this.deviceId = undefined;
|
||||
this.refresh();
|
||||
async loadBasicData() {
|
||||
try {
|
||||
this.loading = true;
|
||||
const [energyTypeRes, meterRes] = await Promise.all([
|
||||
listEnergyType({ pageSize: 999 }),
|
||||
listMeter({ pageSize: 999, isTotalMeter: 0 })
|
||||
])
|
||||
this.energyTypeList = energyTypeRes.rows || []
|
||||
this.meterList = meterRes.rows || []
|
||||
await this.loadData()
|
||||
} catch (error) {
|
||||
console.error('加载基础数据失败', error)
|
||||
}
|
||||
},
|
||||
refresh() {
|
||||
if (this.$refs.overview) {
|
||||
this.$refs.overview.refresh();
|
||||
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;
|
||||
}
|
||||
if (this.$refs.recentTrend) {
|
||||
this.$refs.recentTrend.refresh();
|
||||
},
|
||||
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()
|
||||
this.updateProductionLineChart()
|
||||
this.updateDeviceRankingChart()
|
||||
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' :
|
||||
this.trendChartType === 'week' ? 'YYYY-WW' : 'YYYY-MM'
|
||||
|
||||
const groupedData = new Map()
|
||||
this.energyRecords.forEach(record => {
|
||||
const dateKey = dayjs(record.recordDate).format(dateFormat)
|
||||
const energyType = this.getEnergyTypeName(record.energyId)
|
||||
|
||||
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))))
|
||||
|
||||
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
|
||||
}
|
||||
if (this.$refs.yearToYear) {
|
||||
this.$refs.yearToYear.refresh();
|
||||
this.trendChart.setOption(option)
|
||||
},
|
||||
updateEnergyTypeChart() {
|
||||
const typeMap = new Map()
|
||||
this.energyRecords.forEach(record => {
|
||||
const typeName = this.getEnergyTypeName(record.energyId)
|
||||
typeMap.set(typeName, (typeMap.get(typeName) || 0) + (Number(record.consumption) || 0))
|
||||
})
|
||||
|
||||
const data = Array.from(typeMap.entries()).map(([name, value]) => ({ name, value }))
|
||||
|
||||
const option = {
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
formatter: '{a} <br/>{b}: {c} ({d}%)'
|
||||
},
|
||||
legend: {
|
||||
orient: 'vertical',
|
||||
right: 10,
|
||||
top: 'center'
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '能源类型',
|
||||
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
|
||||
}
|
||||
]
|
||||
}
|
||||
if (this.$refs.monthToMonth) {
|
||||
this.$refs.monthToMonth.refresh();
|
||||
this.energyTypeChart.setOption(option)
|
||||
},
|
||||
updateProductionLineChart() {
|
||||
const lineMap = new Map()
|
||||
|
||||
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)
|
||||
lineMap.set(lineName, (lineMap.get(lineName) || 0) + (Number(record.consumption) || 0))
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
const sortedLines = Array.from(lineMap.entries())
|
||||
.sort((a, b) => b[1] - a[1])
|
||||
|
||||
const option = {
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'shadow'
|
||||
}
|
||||
},
|
||||
grid: {
|
||||
left: '3%',
|
||||
right: '4%',
|
||||
bottom: '3%',
|
||||
containLabel: true
|
||||
},
|
||||
xAxis: {
|
||||
type: 'value',
|
||||
name: '能耗'
|
||||
},
|
||||
yAxis: {
|
||||
type: 'category',
|
||||
data: sortedLines.map(item => item[0])
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '能耗',
|
||||
type: 'bar',
|
||||
data: sortedLines.map(item => item[1]),
|
||||
itemStyle: {
|
||||
color: new echarts.graphic.LinearGradient(0, 0, 1, 0, [
|
||||
{ offset: 0, color: '#409EFF' },
|
||||
{ offset: 1, color: '#00CED1' }
|
||||
]),
|
||||
borderRadius: [0, 4, 4, 0]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
this.productionLineChart.setOption(option)
|
||||
},
|
||||
updateDeviceRankingChart() {
|
||||
const deviceMap = new Map()
|
||||
|
||||
this.energyRecords.forEach(record => {
|
||||
const meter = this.meterList.find(m => m.meterId === record.meterId)
|
||||
const deviceName = meter ? meter.meterCode : `设备${record.meterId}`
|
||||
deviceMap.set(deviceName, (deviceMap.get(deviceName) || 0) + (Number(record.consumption) || 0))
|
||||
})
|
||||
|
||||
const sortedDevices = Array.from(deviceMap.entries())
|
||||
.sort((a, b) => b[1] - a[1])
|
||||
.slice(0, 10)
|
||||
|
||||
const option = {
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'shadow'
|
||||
}
|
||||
},
|
||||
grid: {
|
||||
left: '3%',
|
||||
right: '4%',
|
||||
bottom: '3%',
|
||||
containLabel: true
|
||||
},
|
||||
xAxis: {
|
||||
type: 'value',
|
||||
name: '能耗'
|
||||
},
|
||||
yAxis: {
|
||||
type: 'category',
|
||||
data: sortedDevices.map(item => item[0]).reverse()
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '能耗',
|
||||
type: 'bar',
|
||||
data: sortedDevices.map(item => item[1]).reverse(),
|
||||
itemStyle: {
|
||||
color: new echarts.graphic.LinearGradient(0, 0, 1, 0, [
|
||||
{ offset: 0, color: '#67C23A' },
|
||||
{ offset: 1, color: '#95E67D' }
|
||||
]),
|
||||
borderRadius: [0, 4, 4, 0]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
this.deviceRankingChart.setOption(option)
|
||||
},
|
||||
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
|
||||
},
|
||||
updateTableData() {
|
||||
const lineEnergyMap = new Map()
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
})
|
||||
},
|
||||
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;
|
||||
}
|
||||
&.stat-1 {
|
||||
border-left: 4px solid #67C23A;
|
||||
}
|
||||
&.stat-2 {
|
||||
border-left: 4px solid #E6A23C;
|
||||
}
|
||||
&.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;
|
||||
}
|
||||
.stat-1 & {
|
||||
background: linear-gradient(135deg, #67C23A, #95E67D);
|
||||
color: #ffffff;
|
||||
}
|
||||
.stat-2 & {
|
||||
background: linear-gradient(135deg, #E6A23C, #F7D94C);
|
||||
color: #ffffff;
|
||||
}
|
||||
.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 {
|
||||
.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%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</style>
|
||||
|
||||
@@ -1,608 +0,0 @@
|
||||
<template>
|
||||
<div class="month-on-month-analysis">
|
||||
<!-- 筛选区域:周/月/年切换 + 单周期选择 -->
|
||||
<div class="filter-section">
|
||||
<el-row :gutter="20" align="middle">
|
||||
<!-- 时间类型切换(默认选中“按周”) -->
|
||||
<el-col :span="6">
|
||||
<el-select
|
||||
v-model="timeType"
|
||||
placeholder="请选择时间类型"
|
||||
@change="handleTimeTypeChange"
|
||||
clearable
|
||||
>
|
||||
<el-option label="按周" value="week"></el-option>
|
||||
<el-option label="按月" value="month"></el-option>
|
||||
<el-option label="按年" value="year"></el-option>
|
||||
</el-select>
|
||||
</el-col>
|
||||
|
||||
<!-- 日期选择器(单周期选择,非范围) -->
|
||||
<el-col :span="12">
|
||||
<!-- 1. 按周:单周选择器 -->
|
||||
<el-date-picker
|
||||
v-if="timeType === 'week'"
|
||||
v-model="dateRange"
|
||||
type="date"
|
||||
placeholder="选择周"
|
||||
format="yyyy年第WW周"
|
||||
value-format="yyyy-'W'WW"
|
||||
picker-options="{ type: 'week' }"
|
||||
:disabled-date="disableFutureDate"
|
||||
></el-date-picker>
|
||||
|
||||
<!-- 2. 按月:单月选择器 -->
|
||||
<el-date-picker
|
||||
v-else-if="timeType === 'month'"
|
||||
v-model="dateRange"
|
||||
type="date"
|
||||
placeholder="选择月"
|
||||
format="yyyy-MM"
|
||||
value-format="yyyy-MM"
|
||||
picker-options="{ type: 'month' }"
|
||||
:disabled-date="disableFutureDate"
|
||||
></el-date-picker>
|
||||
|
||||
<!-- 3. 按年:单年选择器 -->
|
||||
<el-date-picker
|
||||
v-else-if="timeType === 'year'"
|
||||
v-model="dateRange"
|
||||
type="date"
|
||||
placeholder="选择年"
|
||||
format="yyyy"
|
||||
value-format="yyyy"
|
||||
picker-options="{ type: 'year' }"
|
||||
:disabled-date="disableFutureDate"
|
||||
></el-date-picker>
|
||||
</el-col>
|
||||
|
||||
<!-- 查询/重置按钮(无选择时禁用查询) -->
|
||||
<el-col :span="6">
|
||||
<el-button type="primary" @click="handleQuery" :disabled="!dateRange">查询</el-button>
|
||||
<el-button type="text" @click="handleReset" style="margin-left: 10px;">重置</el-button>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
|
||||
<!-- 加载状态 -->
|
||||
<div v-if="loading" class="loading-container">
|
||||
<el-loading-spinner></el-loading-spinner>
|
||||
<p>数据加载中...</p>
|
||||
</div>
|
||||
|
||||
<!-- 环比指标卡区域(保持原有展示逻辑) -->
|
||||
<div v-else class="indicator-cards">
|
||||
<el-row :gutter="20">
|
||||
<!-- 当期值 -->
|
||||
<el-col :span="6">
|
||||
<div class="indicator-card">
|
||||
<div class="card-header">
|
||||
<span class="card-title">当期值</span>
|
||||
<span class="card-period">{{ currentPeriodText }}</span>
|
||||
</div>
|
||||
<div class="card-value">{{ formatNumber(currentValue) }} {{ unit }}</div>
|
||||
<div class="card-desc">当前{{ timeTypeMap[timeType] }}的{{ indicatorName }}</div>
|
||||
</div>
|
||||
</el-col>
|
||||
|
||||
<!-- 上期值 -->
|
||||
<el-col :span="6">
|
||||
<div class="indicator-card">
|
||||
<div class="card-header">
|
||||
<span class="card-title">上期值</span>
|
||||
<span class="card-period">{{ previousPeriodText }}</span>
|
||||
</div>
|
||||
<div class="card-value">{{ formatNumber(previousValue) }} {{ unit }}</div>
|
||||
<div class="card-desc">上一个{{ timeTypeMap[timeType] }}的{{ indicatorName }}</div>
|
||||
</div>
|
||||
</el-col>
|
||||
|
||||
<!-- 增加值 -->
|
||||
<el-col :span="6">
|
||||
<div class="indicator-card">
|
||||
<div class="card-header">
|
||||
<span class="card-title">增加值</span>
|
||||
<span class="card-icon"><i class="el-icon-arrow-right"></i></span>
|
||||
</div>
|
||||
<div class="card-value" :class="increaseValue > 0 ? 'text-increase' : increaseValue < 0 ? 'text-decrease' : ''">
|
||||
{{ formatNumber(increaseValue) }} {{ unit }}
|
||||
</div>
|
||||
<div class="card-desc">当期 - 上期</div>
|
||||
</div>
|
||||
</el-col>
|
||||
|
||||
<!-- 环比 -->
|
||||
<el-col :span="6">
|
||||
<div class="indicator-card">
|
||||
<div class="card-header">
|
||||
<span class="card-title">环比</span>
|
||||
<span class="card-icon"><i class="el-icon-refresh"></i></span>
|
||||
</div>
|
||||
<div class="card-value" :class="monthOnMonth > 0 ? 'text-increase' : monthOnMonth < 0 ? 'text-decrease' : ''">
|
||||
{{ monthOnMonth !== null ? (monthOnMonth * 100).toFixed(2) + '%' : '--' }}
|
||||
</div>
|
||||
<div class="card-desc">(当期 - 上期)/ 上期 × 100%</div>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'CycleAnalysis',
|
||||
props: {
|
||||
// 数据单位(如:kWh、Nm³、元)
|
||||
unit: {
|
||||
type: String,
|
||||
default: 'kWh'
|
||||
},
|
||||
// 指标名称(如:能耗、费用、产量)
|
||||
indicatorName: {
|
||||
type: String,
|
||||
default: '能耗'
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
// 时间类型:默认按周(week)
|
||||
timeType: 'week',
|
||||
// 单周期选择值(格式:week→yyyy-Www,month→yyyy-MM,year→yyyy)
|
||||
dateRange: '',
|
||||
// 加载状态
|
||||
loading: false,
|
||||
// 核心数据
|
||||
currentValue: null, // 当期值
|
||||
previousValue: null, // 上期值
|
||||
increaseValue: null, // 增加值(当期-上期)
|
||||
monthOnMonth: null, // 环比((当期-上期)/上期)
|
||||
// 周期文本描述(如:2025年第23周、2025年06月)
|
||||
currentPeriodText: '',
|
||||
previousPeriodText: '',
|
||||
// 时间类型映射(用于文案显示)
|
||||
timeTypeMap: {
|
||||
week: '周',
|
||||
month: '月',
|
||||
year: '年'
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
// 初始化默认周期(当前周/月/年)+ 自动查询一次
|
||||
this.initDateRange()
|
||||
this.handleQuery()
|
||||
},
|
||||
methods: {
|
||||
/**
|
||||
* 初始化默认周期(当前时间类型的当前周期)
|
||||
*/
|
||||
initDateRange() {
|
||||
const now = new Date()
|
||||
const year = now.getFullYear()
|
||||
let currentPeriod
|
||||
|
||||
switch (this.timeType) {
|
||||
case 'week':
|
||||
// 周格式:yyyy-Www(两位周数)
|
||||
const [currentWeek] = this.getWeekInfo(now)
|
||||
currentPeriod = `${year}-W${this.padZero(currentWeek)}`
|
||||
break
|
||||
|
||||
case 'month':
|
||||
// 月格式:yyyy-MM(两位月份)
|
||||
const currentMonth = now.getMonth() + 1
|
||||
currentPeriod = `${year}-${this.padZero(currentMonth)}`
|
||||
break
|
||||
|
||||
case 'year':
|
||||
// 年格式:yyyy
|
||||
currentPeriod = `${year}`
|
||||
break
|
||||
}
|
||||
|
||||
this.dateRange = currentPeriod
|
||||
// 同步更新周期文本(当期+上期)
|
||||
this.updatePeriodText()
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取周信息(本年周数 + 周起始日期)
|
||||
* @param {Date} date - 日期对象
|
||||
* @returns {Array} [周数, 周起始日期(Date)]
|
||||
*/
|
||||
getWeekInfo(date) {
|
||||
const firstDayOfYear = new Date(date.getFullYear(), 0, 1)
|
||||
const pastDaysOfYear = (date - firstDayOfYear) / (24 * 60 * 60 * 1000)
|
||||
// 周日为一周第一天,计算本年周数
|
||||
const weekNumber = Math.ceil((pastDaysOfYear + firstDayOfYear.getDay() + 1) / 7)
|
||||
// 计算当前周的起始日期(周日)
|
||||
const weekStart = new Date(date)
|
||||
weekStart.setDate(date.getDate() - date.getDay())
|
||||
return [weekNumber, weekStart]
|
||||
},
|
||||
|
||||
/**
|
||||
* 计算上期周期(根据当前周期自动推导)
|
||||
* @param {String} currentPeriod - 当前周期(如:2025-W23、2025-06、2025)
|
||||
* @param {String} timeType - 时间类型(week/month/year)
|
||||
* @returns {String} 上期周期
|
||||
*/
|
||||
getPreviousPeriod(currentPeriod, timeType) {
|
||||
const [year, part] = currentPeriod.split(
|
||||
timeType === 'week' ? '-W' : timeType === 'month' ? '-' : ''
|
||||
)
|
||||
let prevYear = parseInt(year)
|
||||
let prevPart
|
||||
|
||||
switch (timeType) {
|
||||
case 'week':
|
||||
const currentWeek = parseInt(part)
|
||||
// 若当前是第1周,上期为上一年最后一周
|
||||
if (currentWeek === 1) {
|
||||
prevYear -= 1
|
||||
prevPart = this.getLastWeekOfYear(prevYear)
|
||||
} else {
|
||||
prevPart = currentWeek - 1
|
||||
}
|
||||
return `${prevYear}-W${this.padZero(prevPart)}`
|
||||
|
||||
case 'month':
|
||||
const currentMonth = parseInt(part)
|
||||
// 若当前是1月,上期为上一年12月
|
||||
if (currentMonth === 1) {
|
||||
prevYear -= 1
|
||||
prevPart = 12
|
||||
} else {
|
||||
prevPart = currentMonth - 1
|
||||
}
|
||||
return `${prevYear}-${this.padZero(prevPart)}`
|
||||
|
||||
case 'year':
|
||||
// 上期为上一年
|
||||
return `${prevYear - 1}`
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取某一年的最后一周(处理跨年周场景)
|
||||
* @param {Number} year - 年份
|
||||
* @returns {Number} 最后一周的周数
|
||||
*/
|
||||
getLastWeekOfYear(year) {
|
||||
const lastDayOfYear = new Date(year, 11, 31) // 12月31日
|
||||
const [lastWeek] = this.getWeekInfo(lastDayOfYear)
|
||||
return lastWeek
|
||||
},
|
||||
|
||||
/**
|
||||
* 将周期转换为时间段(startDate ~ endDate,格式:yyyy-MM-dd)
|
||||
* @param {String} period - 周期(如:2025-W23、2025-06、2025)
|
||||
* @param {String} timeType - 时间类型(week/month/year)
|
||||
* @returns {Object} { start: 开始日期, end: 结束日期 }
|
||||
*/
|
||||
getPeriodTimeRange(period, timeType) {
|
||||
let startDate, endDate
|
||||
const [year, part] = period.split(
|
||||
timeType === 'week' ? '-W' : timeType === 'month' ? '-' : ''
|
||||
)
|
||||
const targetYear = parseInt(year)
|
||||
|
||||
switch (timeType) {
|
||||
case 'week':
|
||||
const week = parseInt(part)
|
||||
// 计算该年第N周的起始日期(周日)
|
||||
const firstDayOfYear = new Date(targetYear, 0, 1)
|
||||
const firstWeekStart = new Date(firstDayOfYear)
|
||||
firstWeekStart.setDate(firstDayOfYear.getDate() - firstDayOfYear.getDay())
|
||||
// 第N周起始 = 第一周起始 + (N-1)*7天
|
||||
startDate = new Date(firstWeekStart)
|
||||
startDate.setDate(firstWeekStart.getDate() + (week - 1) * 7)
|
||||
// 第N周结束 = 起始 + 6天(周六)
|
||||
endDate = new Date(startDate)
|
||||
endDate.setDate(startDate.getDate() + 6)
|
||||
break
|
||||
|
||||
case 'month':
|
||||
const month = parseInt(part) - 1 // 月份从0开始
|
||||
startDate = new Date(targetYear, month, 1) // 当月1号
|
||||
endDate = new Date(targetYear, month + 1, 0) // 当月最后一天
|
||||
break
|
||||
|
||||
case 'year':
|
||||
startDate = new Date(targetYear, 0, 1) // 1月1号
|
||||
endDate = new Date(targetYear, 11, 31) // 12月31号
|
||||
break
|
||||
}
|
||||
|
||||
// 格式化为 yyyy-MM-dd
|
||||
const format = (date) => date.toISOString().split('T')[0]
|
||||
return { start: format(startDate), end: format(endDate) }
|
||||
},
|
||||
|
||||
/**
|
||||
* 更新周期文本描述(当期+上期)
|
||||
*/
|
||||
updatePeriodText() {
|
||||
if (!this.dateRange) return
|
||||
const currentPeriod = this.dateRange
|
||||
const previousPeriod = this.getPreviousPeriod(currentPeriod, this.timeType)
|
||||
|
||||
switch (this.timeType) {
|
||||
case 'week':
|
||||
const [currWYear, currW] = currentPeriod.split('-W')
|
||||
const [prevWYear, prevW] = previousPeriod.split('-W')
|
||||
this.currentPeriodText = `${currWYear}年第${currW}周`
|
||||
this.previousPeriodText = `${prevWYear}年第${prevW}周`
|
||||
break
|
||||
|
||||
case 'month':
|
||||
const [currMYear, currM] = currentPeriod.split('-')
|
||||
const [prevMYear, prevM] = previousPeriod.split('-')
|
||||
this.currentPeriodText = `${currMYear}年${currM}月`
|
||||
this.previousPeriodText = `${prevMYear}年${prevM}月`
|
||||
break
|
||||
|
||||
case 'year':
|
||||
this.currentPeriodText = `${currentPeriod}年`
|
||||
this.previousPeriodText = `${previousPeriod}年`
|
||||
break
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 时间类型切换:重置周期 + 自动查询
|
||||
*/
|
||||
handleTimeTypeChange() {
|
||||
this.dateRange = ''
|
||||
this.initDateRange()
|
||||
this.resetData()
|
||||
this.handleQuery()
|
||||
},
|
||||
|
||||
/**
|
||||
* 查询按钮:构建参数 + 调用接口 + 计算环比
|
||||
*/
|
||||
handleQuery() {
|
||||
this.loading = true
|
||||
// 1. 构建接口参数(含当期/上期时间段)
|
||||
const apiParams = this.buildApiParams()
|
||||
// 2. 调用接口(实际项目替换为axios)
|
||||
this.mockApiRequest(apiParams)
|
||||
.then(({ currentValue, previousValue }) => {
|
||||
// 3. 保存原始数据(保留2位小数)
|
||||
this.currentValue = Number(currentValue.toFixed(2))
|
||||
this.previousValue = Number(previousValue.toFixed(2))
|
||||
// 4. 计算增加值和环比
|
||||
this.calculateIndicators()
|
||||
// 5. 更新周期文本
|
||||
this.updatePeriodText()
|
||||
})
|
||||
.catch(err => {
|
||||
this.$message.error(`数据获取失败:${err.message}`)
|
||||
this.resetData()
|
||||
})
|
||||
.finally(() => {
|
||||
this.loading = false
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* 构建接口参数(核心:传递当期/上期时间段)
|
||||
* @returns {Object} 接口请求参数
|
||||
*/
|
||||
buildApiParams() {
|
||||
if (!this.dateRange) return {}
|
||||
// 当前周期 + 上期周期
|
||||
const currentPeriod = this.dateRange
|
||||
const previousPeriod = this.getPreviousPeriod(currentPeriod, this.timeType)
|
||||
// 转换为时间段(start/end: yyyy-MM-dd)
|
||||
const currentTimeRange = this.getPeriodTimeRange(currentPeriod, this.timeType)
|
||||
const previousTimeRange = this.getPeriodTimeRange(previousPeriod, this.timeType)
|
||||
|
||||
return {
|
||||
indicatorName: this.indicatorName, // 指标名称(如:能耗)
|
||||
timeType: this.timeType, // 时间类型(week/month/year)
|
||||
currentRange: currentTimeRange, // 当期时间段
|
||||
previousRange: previousTimeRange // 上期时间段
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 模拟接口请求(实际项目替换为axios.post/get)
|
||||
* @param {Object} params - 接口参数
|
||||
* @returns {Promise} 包含当期/上期值的Promise
|
||||
*/
|
||||
mockApiRequest(params) {
|
||||
return new Promise((resolve) => {
|
||||
// 模拟网络延迟
|
||||
setTimeout(() => {
|
||||
// 根据时间类型生成合理范围的随机数据(以上期为基础)
|
||||
let baseValue = 0
|
||||
switch (params.timeType) {
|
||||
case 'week':
|
||||
baseValue = 7000 + Math.random() * 2000 // 周数据:7000-9000
|
||||
break
|
||||
case 'month':
|
||||
baseValue = 30000 + Math.random() * 5000 // 月数据:30000-35000
|
||||
break
|
||||
case 'year':
|
||||
baseValue = 365000 + Math.random() * 50000 // 年数据:36.5万-41.5万
|
||||
break
|
||||
}
|
||||
|
||||
// 当期值 = 上期值 × 随机波动(-5% ~ +15%)
|
||||
const previousValue = baseValue
|
||||
const fluctuation = -0.05 + Math.random() * 0.2 // 波动范围:-5% ~ +15%
|
||||
const currentValue = previousValue * (1 + fluctuation)
|
||||
|
||||
resolve({ currentValue, previousValue })
|
||||
}, 800)
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* 计算增加值和环比(处理上期为0的异常场景)
|
||||
*/
|
||||
calculateIndicators() {
|
||||
// 增加值 = 当期 - 上期
|
||||
this.increaseValue = this.currentValue - this.previousValue
|
||||
// 环比 = (当期 - 上期) / 上期(上期为0时特殊处理)
|
||||
if (this.previousValue === 0) {
|
||||
this.monthOnMonth = this.currentValue > 0 ? 1 : 0 // 上期为0时,当期有值则环比100%
|
||||
} else {
|
||||
this.monthOnMonth = this.increaseValue / this.previousValue
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 重置:恢复默认周期 + 清空数据 + 自动查询
|
||||
*/
|
||||
handleReset() {
|
||||
this.initDateRange()
|
||||
this.resetData()
|
||||
this.handleQuery()
|
||||
},
|
||||
|
||||
/**
|
||||
* 清空核心数据
|
||||
*/
|
||||
resetData() {
|
||||
this.currentValue = null
|
||||
this.previousValue = null
|
||||
this.increaseValue = null
|
||||
this.monthOnMonth = null
|
||||
this.currentPeriodText = ''
|
||||
this.previousPeriodText = ''
|
||||
},
|
||||
|
||||
/**
|
||||
* 数字补零(确保周数/月份为两位)
|
||||
* @param {Number} num - 需补零的数字
|
||||
* @returns {String} 补零后的字符串
|
||||
*/
|
||||
padZero(num) {
|
||||
return num < 10 ? `0${num}` : `${num}`
|
||||
},
|
||||
|
||||
/**
|
||||
* 格式化数字(千分位分隔 + 保留2位小数)
|
||||
* @param {Number} num - 需格式化的数字
|
||||
* @returns {String} 格式化后的字符串
|
||||
*/
|
||||
formatNumber(num) {
|
||||
if (num === null || isNaN(num)) return '--'
|
||||
return num.toLocaleString('zh-CN', {
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* 禁用未来日期选择(无法选择未到的周/月/年)
|
||||
* @param {Date} date - 待判断日期
|
||||
* @returns {Boolean} 是否禁用
|
||||
*/
|
||||
disableFutureDate(date) {
|
||||
return date > new Date()
|
||||
},
|
||||
|
||||
refresh() {
|
||||
this.handleQuery()
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.month-on-month-analysis {
|
||||
padding: 20px;
|
||||
background-color: #fff;
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.filter-section {
|
||||
margin-bottom: 20px;
|
||||
padding-bottom: 15px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.indicator-cards {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.indicator-card {
|
||||
background-color: #fff;
|
||||
border-radius: 6px;
|
||||
padding: 18px;
|
||||
height: 100%;
|
||||
border: 1px solid #f0f0f0;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.indicator-card:hover {
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.card-period {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
background-color: #f5f7fa;
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.card-icon {
|
||||
color: #409eff;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.card-value {
|
||||
font-size: 26px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 6px;
|
||||
transition: color 0.3s ease;
|
||||
}
|
||||
|
||||
.card-desc {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
/* 增加值/环比颜色:上升红、下降绿 */
|
||||
.text-increase {
|
||||
color: #f56c6c;
|
||||
}
|
||||
|
||||
.text-decrease {
|
||||
color: #67c23a;
|
||||
}
|
||||
|
||||
/* 加载状态 */
|
||||
.loading-container {
|
||||
text-align: center;
|
||||
padding: 60px 0;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.loading-container p {
|
||||
margin-top: 12px;
|
||||
font-size: 14px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,320 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<!-- 今日 vs 昨日 行 -->
|
||||
<div class="trend-row">
|
||||
<div class="data-card">
|
||||
<div class="data-value">{{ data.today }}</div>
|
||||
<div class="data-label">今日</div>
|
||||
</div>
|
||||
<div class="data-card">
|
||||
<div class="data-value">{{ data.yesterday }}</div>
|
||||
<div class="data-label">昨日</div>
|
||||
</div>
|
||||
<div class="trend-card" :class="todayTrendClass">
|
||||
<div class="trend-indicator">
|
||||
<i class="fas" :class="todayTrendIcon"></i>
|
||||
</div>
|
||||
<div class="trend-percent">{{ todayPercent }}</div>
|
||||
<div class="trend-diff">{{ todayDiff }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 本月 vs 上月 行 -->
|
||||
<div class="trend-row">
|
||||
<div class="data-card">
|
||||
<div class="data-value">{{ data.currentMonth }}</div>
|
||||
<div class="data-label">本月</div>
|
||||
</div>
|
||||
<div class="data-card">
|
||||
<div class="data-value">{{ data.lastMonth }}</div>
|
||||
<div class="data-label">上月</div>
|
||||
</div>
|
||||
<div class="trend-card" :class="monthTrendClass">
|
||||
<div class="trend-indicator">
|
||||
<i class="fas" :class="monthTrendIcon"></i>
|
||||
</div>
|
||||
<div class="trend-percent">{{ monthPercent }}</div>
|
||||
<div class="trend-diff">{{ monthDiff }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 本年 vs 去年 行 -->
|
||||
<div class="trend-row">
|
||||
<div class="data-card">
|
||||
<div class="data-value">{{ data.currentYear }}</div>
|
||||
<div class="data-label">本年</div>
|
||||
</div>
|
||||
<div class="data-card">
|
||||
<div class="data-value">{{ data.lastYear }}</div>
|
||||
<div class="data-label">去年</div>
|
||||
</div>
|
||||
<div class="trend-card" :class="yearTrendClass">
|
||||
<div class="trend-indicator">
|
||||
<i class="fas" :class="yearTrendIcon"></i>
|
||||
</div>
|
||||
<div class="trend-percent">{{ yearPercent }}</div>
|
||||
<div class="trend-diff">{{ yearDiff }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "TrendComparison",
|
||||
props: {
|
||||
unit: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
energyName: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
energyType: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
locationId: {
|
||||
type: String,
|
||||
required: false
|
||||
},
|
||||
deviceId: {
|
||||
type: String,
|
||||
required: false
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
data: {} // 存储模拟数据
|
||||
};
|
||||
},
|
||||
created() {
|
||||
// 组件创建时获取模拟数据
|
||||
this.data = this.fetchMockData();
|
||||
},
|
||||
methods: {
|
||||
/**
|
||||
* 模拟API请求,生成六个数值
|
||||
* 生成规则:
|
||||
* - 今日:随机100-500之间的数值
|
||||
* - 昨日:基于今日数值上下浮动20%
|
||||
* - 本月:随机5000-20000之间的数值
|
||||
* - 上月:基于本月数值上下浮动30%
|
||||
* - 本年:随机100000-500000之间的数值
|
||||
* - 去年:基于本年数值上下浮动40%
|
||||
*/
|
||||
fetchMockData() {
|
||||
// 生成今日数据
|
||||
const today = parseFloat((Math.random() * 400 + 100).toFixed(2));
|
||||
|
||||
// 昨日数据:今日数据的80%-120%之间
|
||||
const yesterday = parseFloat((today * (0.8 + Math.random() * 0.4)).toFixed(2));
|
||||
|
||||
// 本月数据
|
||||
const currentMonth = parseFloat((Math.random() * 15000 + 5000).toFixed(2));
|
||||
|
||||
// 上月数据:本月数据的70%-130%之间
|
||||
const lastMonth = parseFloat((currentMonth * (0.7 + Math.random() * 0.6)).toFixed(2));
|
||||
|
||||
// 本年数据
|
||||
const currentYear = parseFloat((Math.random() * 400000 + 100000).toFixed(2));
|
||||
|
||||
// 去年数据:本年数据的60%-140%之间
|
||||
const lastYear = parseFloat((currentYear * (0.6 + Math.random() * 0.8)).toFixed(2));
|
||||
|
||||
return {
|
||||
today,
|
||||
yesterday,
|
||||
currentMonth,
|
||||
lastMonth,
|
||||
currentYear,
|
||||
lastYear
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* 格式化数值显示,添加千位分隔符
|
||||
* @param {Number} num 要格式化的数值
|
||||
* @returns {String} 格式化后的字符串
|
||||
*/
|
||||
formatNumber(num) {
|
||||
return num.toLocaleString('zh-CN', {
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2
|
||||
});
|
||||
},
|
||||
|
||||
// 对外暴露的刷新方法
|
||||
refresh() {
|
||||
this.data = this.fetchMockData();
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
// ---- 今日 vs 昨日 计算 ----
|
||||
todayDiff() {
|
||||
const diff = this.data.today - this.data.yesterday;
|
||||
return diff >= 0
|
||||
? `+${this.formatNumber(diff)}`
|
||||
: `${this.formatNumber(diff)}`;
|
||||
},
|
||||
todayPercent() {
|
||||
if (this.data.yesterday === 0) return "0.00%";
|
||||
const percent = ((this.data.today - this.data.yesterday) / this.data.yesterday) * 100;
|
||||
return percent >= 0
|
||||
? `+${percent.toFixed(2)}%`
|
||||
: `${percent.toFixed(2)}%`;
|
||||
},
|
||||
todayTrendClass() {
|
||||
return this.data.today > this.data.yesterday ? "trend-up" : "trend-down";
|
||||
},
|
||||
todayTrendIcon() {
|
||||
return this.data.today > this.data.yesterday ? "fa-arrow-up" : "fa-arrow-down";
|
||||
},
|
||||
|
||||
// ---- 本月 vs 上月 计算 ----
|
||||
monthDiff() {
|
||||
const diff = this.data.currentMonth - this.data.lastMonth;
|
||||
return diff >= 0
|
||||
? `+${this.formatNumber(diff)}`
|
||||
: `${this.formatNumber(diff)}`;
|
||||
},
|
||||
monthPercent() {
|
||||
if (this.data.lastMonth === 0) return "0.00%";
|
||||
const percent = ((this.data.currentMonth - this.data.lastMonth) / this.data.lastMonth) * 100;
|
||||
return percent >= 0
|
||||
? `+${percent.toFixed(2)}%`
|
||||
: `${percent.toFixed(2)}%`;
|
||||
},
|
||||
monthTrendClass() {
|
||||
return this.data.currentMonth > this.data.lastMonth ? "trend-up" : "trend-down";
|
||||
},
|
||||
monthTrendIcon() {
|
||||
return this.data.currentMonth > this.data.lastMonth ? "fa-arrow-up" : "fa-arrow-down";
|
||||
},
|
||||
|
||||
// ---- 本年 vs 去年 计算 ----
|
||||
yearDiff() {
|
||||
const diff = this.data.currentYear - this.data.lastYear;
|
||||
return diff >= 0
|
||||
? `+${this.formatNumber(diff)}`
|
||||
: `${this.formatNumber(diff)}`;
|
||||
},
|
||||
yearPercent() {
|
||||
if (this.data.lastYear === 0) return "0.00%";
|
||||
const percent = ((this.data.currentYear - this.data.lastYear) / this.data.lastYear) * 100;
|
||||
return percent >= 0
|
||||
? `+${percent.toFixed(2)}%`
|
||||
: `${percent.toFixed(2)}%`;
|
||||
},
|
||||
yearTrendClass() {
|
||||
return this.data.currentYear > this.data.lastYear ? "trend-up" : "trend-down";
|
||||
},
|
||||
yearTrendIcon() {
|
||||
return this.data.currentYear > this.data.lastYear ? "fa-arrow-up" : "fa-arrow-down";
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.trend-container {
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
max-width: 800px;
|
||||
margin: 20px auto;
|
||||
padding: 20px;
|
||||
background-color: #ffffff;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.trend-title {
|
||||
color: #333333;
|
||||
font-size: 18px;
|
||||
margin-bottom: 20px;
|
||||
padding-bottom: 10px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.trend-row {
|
||||
display: flex;
|
||||
margin-bottom: 15px;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
|
||||
.trend-row:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.data-card, .trend-card {
|
||||
padding: 15px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.data-card {
|
||||
flex: 1;
|
||||
background-color: #f9fafb;
|
||||
border-right: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.trend-card {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.data-value {
|
||||
font-size: 22px;
|
||||
font-weight: 600;
|
||||
color: #111827;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.data-label {
|
||||
font-size: 14px;
|
||||
color: #6b7280;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.trend-indicator {
|
||||
margin-bottom: 5px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.trend-percent {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 3px;
|
||||
}
|
||||
|
||||
.trend-diff {
|
||||
font-size: 14px;
|
||||
color: #4b5563;
|
||||
}
|
||||
|
||||
/* 趋势样式 */
|
||||
.trend-up {
|
||||
background-color: rgba(16, 185, 129, 0.05);
|
||||
}
|
||||
|
||||
.trend-up .trend-percent,
|
||||
.trend-up .trend-indicator {
|
||||
color: #10b981; /* 绿色表示上升 */
|
||||
}
|
||||
|
||||
.trend-down {
|
||||
background-color: rgba(239, 68, 68, 0.05);
|
||||
}
|
||||
|
||||
.trend-down .trend-percent,
|
||||
.trend-down .trend-indicator {
|
||||
color: #ef4444; /* 红色表示下降 */
|
||||
}
|
||||
</style>
|
||||
@@ -1,382 +0,0 @@
|
||||
<template>
|
||||
<div class="trends-container">
|
||||
<!-- 月度能耗图表区域 -->
|
||||
<el-row class="chart-group">
|
||||
<el-col :span="24">
|
||||
<div class="chart-header">
|
||||
<h3>月度能耗趋势</h3>
|
||||
<div class="chart-type-btn-group">
|
||||
<el-button :type="monthChartType === 'bar' ? 'primary' : 'default'" @click="toggleMonthChartType('bar')"
|
||||
:disabled="!hasMonthData">
|
||||
柱状图
|
||||
</el-button>
|
||||
<el-button :type="monthChartType === 'line' ? 'primary' : 'default'" @click="toggleMonthChartType('line')"
|
||||
:disabled="!hasMonthData">
|
||||
折线图
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="chart-wrapper">
|
||||
<div ref="monthChart" class="chart-container"></div>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<!-- 日度能耗图表区域 -->
|
||||
<el-row class="chart-group">
|
||||
<el-col :span="24">
|
||||
<div class="chart-header">
|
||||
<h3>日度能耗趋势</h3>
|
||||
<div class="chart-type-btn-group">
|
||||
<el-button :type="dayChartType === 'bar' ? 'primary' : 'default'" @click="toggleDayChartType('bar')"
|
||||
:disabled="!hasDayData">
|
||||
柱状图
|
||||
</el-button>
|
||||
<el-button :type="dayChartType === 'line' ? 'primary' : 'default'" @click="toggleDayChartType('line')"
|
||||
:disabled="!hasDayData">
|
||||
折线图
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="chart-wrapper">
|
||||
<div ref="dayChart" class="chart-container"></div>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import * as echarts from 'echarts'
|
||||
import { getRecentEnergySummary } from '@/api/ems/dashboard/timer'
|
||||
|
||||
export default {
|
||||
name: 'RecentTrends',
|
||||
data() {
|
||||
return {
|
||||
monthData: [], // 原始月度数据
|
||||
dayData: [], // 原始日度数据
|
||||
completedMonthData: [], // 补全后的月度数据
|
||||
completedDayData: [], // 补全后的日度数据
|
||||
monthChartType: 'bar',
|
||||
dayChartType: 'bar',
|
||||
monthChart: null,
|
||||
dayChart: null,
|
||||
hasMonthData: false, // 标记是否有月度数据
|
||||
hasDayData: false // 标记是否有日度数据
|
||||
}
|
||||
},
|
||||
props: {
|
||||
unit: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
energyName: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
energyType: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
locationId: {
|
||||
type: String,
|
||||
required: false
|
||||
},
|
||||
deviceId: {
|
||||
type: String,
|
||||
required: false
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
const dayChartDOM = this.$refs.dayChart
|
||||
console.log(dayChartDOM, this.$refs)
|
||||
if (!dayChartDOM) return
|
||||
this.dayChart = echarts.init(dayChartDOM)
|
||||
|
||||
const monthChartDOM = this.$refs.monthChart
|
||||
if (!monthChartDOM) return
|
||||
this.monthChart = echarts.init(monthChartDOM)
|
||||
this.fetchMonthData()
|
||||
this.fetchDayData()
|
||||
window.addEventListener('resize', this.handleWindowResize)
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.destroyCharts()
|
||||
window.removeEventListener('resize', this.handleWindowResize)
|
||||
},
|
||||
methods: {
|
||||
// 销毁图表实例
|
||||
destroyCharts() {
|
||||
if (this.monthChart) {
|
||||
this.monthChart.dispose()
|
||||
this.monthChart = null
|
||||
}
|
||||
if (this.dayChart) {
|
||||
this.dayChart.dispose()
|
||||
this.dayChart = null
|
||||
}
|
||||
},
|
||||
|
||||
// 数据请求
|
||||
fetchMonthData() {
|
||||
const year = new Date().getFullYear()
|
||||
getRecentEnergySummary({ year, energyType: this.energyType, locationId: this.locationId, deviceId: this.deviceId })
|
||||
.then(res => {
|
||||
this.monthData = res.data || []
|
||||
console.log(this.monthData)
|
||||
// 检查是否有有效数据
|
||||
this.hasMonthData = Array.isArray(this.monthData) && this.monthData.length > 0
|
||||
|
||||
this.completedMonthData = this.completeMonthData(this.monthData)
|
||||
this.initMonthChart()
|
||||
})
|
||||
},
|
||||
|
||||
fetchDayData() {
|
||||
const date = new Date()
|
||||
const month = date.getFullYear() + '-' + (date.getMonth() + 1).toString().padStart(2, '0')
|
||||
getRecentEnergySummary({ month, energyType: this.energyType, locationId: this.locationId, deviceId: this.deviceId })
|
||||
.then(res => {
|
||||
this.dayData = res.data || []
|
||||
console.log(this.dayData)
|
||||
|
||||
this.completedDayData = this.completeDayData(this.dayData)
|
||||
this.initDayChart()
|
||||
})
|
||||
},
|
||||
|
||||
// 补全月度数据 - 固定显示12个月份(一整年)
|
||||
completeMonthData(data) {
|
||||
this.hasMonthData = Array.isArray(this.monthData) && this.monthData.length > 0
|
||||
if (!this.hasMonthData) {
|
||||
const result = []
|
||||
for (let i = 0; i < 12; i++) {
|
||||
result.push({ month: i + 1, totalConsumption: 0 })
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
const result = []
|
||||
|
||||
for (let i = 0; i < 12; i++) {
|
||||
// 查找data中month{year}-{month}中{month}为i+1的数据
|
||||
const item = data.find(item => item.month?.split('-')[1].padStart(2, '0') == i + 1)
|
||||
console.log(item, i + 1, data)
|
||||
if (item) {
|
||||
result.push(item)
|
||||
} else {
|
||||
result.push({ month: i + 1, totalConsumption: 0 })
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
},
|
||||
|
||||
// 补全日度数据 - 按照当月实际天数补齐
|
||||
completeDayData(data) {
|
||||
this.hasDayData = Array.isArray(this.dayData) && this.dayData.length > 0
|
||||
|
||||
// 本月天数
|
||||
const today = new Date()
|
||||
const dayCount = new Date(today.getFullYear(), today.getMonth() + 1, 0).getDate()
|
||||
if (!this.hasDayData) {
|
||||
const result = []
|
||||
for (let i = 0; i < dayCount; i++) {
|
||||
result.push({ day: i + 1, totalConsumption: 0 })
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// 补全数据
|
||||
const result = []
|
||||
for (let i = 0; i < dayCount; i++) {
|
||||
const item = data.find(item => item.day?.split('-')[2].padStart(2, '0') == i + 1)
|
||||
if (item) {
|
||||
result.push(item)
|
||||
} else {
|
||||
result.push({ day: i + 1, totalConsumption: 0 })
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
|
||||
// 初始化月度图表
|
||||
initMonthChart() {
|
||||
if (!this.hasMonthData) return
|
||||
|
||||
console.log(this.completedMonthData)
|
||||
const xData = this.completedMonthData.map(item => item.month)
|
||||
const yData = this.completedMonthData.map(item => item.totalConsumption)
|
||||
|
||||
const option = {
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
formatter: '{b}: {c} ' + this.unit
|
||||
},
|
||||
grid: {
|
||||
left: '3%',
|
||||
right: '4%',
|
||||
bottom: '3%',
|
||||
containLabel: true
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: xData,
|
||||
axisLabel: {
|
||||
rotate: 15
|
||||
}
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
name: '能耗 (' + this.unit + ')',
|
||||
min: 0
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '总能耗',
|
||||
type: this.monthChartType,
|
||||
data: yData,
|
||||
itemStyle: {
|
||||
color: this.monthChartType === 'bar' ? '#409EFF' : '#67C23A'
|
||||
},
|
||||
markPoint: {
|
||||
data: [
|
||||
{ type: 'max', name: '最大值', itemStyle: { color: '#F56C6C' } },
|
||||
{ type: 'min', name: '最小值', itemStyle: { color: '#909399' } }
|
||||
],
|
||||
label: {
|
||||
formatter: '{b}: {c} ' + this.unit
|
||||
}
|
||||
},
|
||||
smooth: this.monthChartType === 'line'
|
||||
}
|
||||
]
|
||||
}
|
||||
console.log(option)
|
||||
this.monthChart.setOption(option)
|
||||
},
|
||||
|
||||
// 初始化日度图表
|
||||
initDayChart() {
|
||||
if (!this.hasDayData) return
|
||||
|
||||
const xData = this.completedDayData.map(item => item.day)
|
||||
const yData = this.completedDayData.map(item => item.totalConsumption)
|
||||
|
||||
const option = {
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
formatter: '{b}: {c} ' + this.unit
|
||||
},
|
||||
grid: {
|
||||
left: '3%',
|
||||
right: '4%',
|
||||
bottom: '3%',
|
||||
containLabel: true
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: xData,
|
||||
axisLabel: {
|
||||
rotate: 15
|
||||
}
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
name: '能耗 (' + this.unit + ')',
|
||||
min: 0
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '总能耗',
|
||||
type: this.dayChartType,
|
||||
data: yData,
|
||||
itemStyle: {
|
||||
color: this.dayChartType === 'bar' ? '#409EFF' : '#67C23A'
|
||||
},
|
||||
markPoint: {
|
||||
data: [
|
||||
{ type: 'max', name: '最大值', itemStyle: { color: '#F56C6C' } },
|
||||
{ type: 'min', name: '最小值', itemStyle: { color: '#909399' } }
|
||||
],
|
||||
label: {
|
||||
formatter: '{b}: {c} ' + this.unit
|
||||
}
|
||||
},
|
||||
smooth: this.dayChartType === 'line'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
this.dayChart.setOption(option)
|
||||
},
|
||||
|
||||
// 切换图表类型
|
||||
toggleMonthChartType(type) {
|
||||
this.monthChartType = type
|
||||
this.initMonthChart()
|
||||
},
|
||||
toggleDayChartType(type) {
|
||||
this.dayChartType = type
|
||||
this.initDayChart()
|
||||
},
|
||||
|
||||
// 窗口大小调整
|
||||
handleWindowResize() {
|
||||
if (this.monthChart) this.monthChart.resize()
|
||||
if (this.dayChart) this.dayChart.resize()
|
||||
},
|
||||
|
||||
// 对外暴露的刷新方法
|
||||
refresh() {
|
||||
this.fetchMonthData()
|
||||
this.fetchDayData()
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.trends-container {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.chart-group {
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.chart-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.chart-type-btn-group button {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.chart-wrapper {
|
||||
width: 100%;
|
||||
height: 300px;
|
||||
border: 1px solid #eee;
|
||||
border-radius: 4px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.chart-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.no-data {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
</style>
|
||||
@@ -1,340 +0,0 @@
|
||||
<template>
|
||||
<div class="energy-analysis-container">
|
||||
<!-- 年份选择与查询按钮区域(增加年份格式验证) -->
|
||||
<div class="query-section">
|
||||
<el-date-picker v-model="year" type="year" value-format="yyyy" placeholder="选择年份" style="margin-right: 10px;"/>
|
||||
<el-button type="primary" @click="handleQuery">查询</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 图表区域(新增刷新按钮,优化图例布局) -->
|
||||
<div class="chart-section">
|
||||
<el-row :gutter="10" align="middle" style="margin-bottom: 10px;">
|
||||
<!-- 图例 -->
|
||||
<el-col :span="4">
|
||||
<div class="legend-item">
|
||||
<span class="legend-dot" style="background-color: #409EFF;"></span>
|
||||
<span>本期({{ year }}年)</span>
|
||||
</div>
|
||||
<div class="legend-item">
|
||||
<span class="legend-dot" style="background-color: #C084FC;"></span>
|
||||
<span>同期({{ Number(year) - 1 }}年)</span>
|
||||
</div>
|
||||
</el-col>
|
||||
<!-- 图表类型切换 + 刷新按钮 -->
|
||||
<el-col :span="20" class="chart-operation">
|
||||
<el-button-group>
|
||||
<el-button type="text" :class="{ active: chartType === 'bar' }" @click="chartType = 'bar'">柱状图</el-button>
|
||||
<el-button type="text" :class="{ active: chartType === 'line' }" @click="chartType = 'line'">折线图</el-button>
|
||||
<el-button type="text" @click="handleRefresh">
|
||||
<i class="el-icon-refresh"></i> 刷新数据
|
||||
</el-button>
|
||||
</el-button-group>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<!-- ECharts 容器(增加无数据提示) -->
|
||||
<div ref="chartRef" class="echarts-box" />
|
||||
</div>
|
||||
|
||||
<!-- 同比分析表格(格式化数据显示) -->
|
||||
<div class="table-section">
|
||||
<el-table :data="tableData" border style="width: 100%" :cell-style="{ textAlign: 'center' }">
|
||||
<el-table-column prop="periodTime" label="本期时间" align="center" />
|
||||
<el-table-column :label="'本期能耗(' + unit + ')'" align="center">
|
||||
<template #default="scope">
|
||||
{{ scope.row.currentEnergy !== null ? scope.row.currentEnergy : '--' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="'同期能耗(' + unit + ')'" align="center">
|
||||
<template #default="scope">
|
||||
{{ scope.row.samePeriodEnergy !== null ? scope.row.samePeriodEnergy : '--' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="同比(%)" align="center">
|
||||
<template #default="scope">
|
||||
<span :class="scope.row.yearOnYear !== null ? (scope.row.yearOnYear > 0 ? 'text-red' : 'text-green') : ''">
|
||||
{{ scope.row.yearOnYear !== null ? (scope.row.yearOnYear * 100).toFixed(2) + '%' : '--' }}
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import * as echarts from 'echarts'
|
||||
|
||||
export default {
|
||||
name: 'YearOnYearAnalysis',
|
||||
props: {
|
||||
energyType: { type: String, default: '' },
|
||||
locationId: { type: String, default: '' },
|
||||
deviceId: { type: String, default: '' },
|
||||
unit: { type: String, default: 'Nm3' }, // 能耗单位,默认Nm3
|
||||
energyName: { type: String, default: '天然气' }
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
year: new Date().getFullYear().toString(), // 默认当前年份
|
||||
chartType: 'bar', // 默认柱状图
|
||||
// 统一数据源:表格和图表共用此数据
|
||||
tableData: Array.from({ length: 12 }, (_, i) => ({
|
||||
periodTime: `${i + 1}月`, // 月份(1月-12月)
|
||||
currentEnergy: null, // 本期能耗(当前年份当月)
|
||||
samePeriodEnergy: null, // 同期能耗(上一年当月)
|
||||
yearOnYear: null // 同比((本期-同期)/同期)
|
||||
})),
|
||||
chartInstance: null // ECharts实例
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.initChart()
|
||||
// 窗口大小变化监听
|
||||
window.addEventListener('resize', this.handleResize)
|
||||
},
|
||||
beforeDestroy() {
|
||||
// 销毁ECharts实例,避免内存泄漏
|
||||
if (this.chartInstance) {
|
||||
this.chartInstance.dispose()
|
||||
}
|
||||
window.removeEventListener('resize', this.handleResize)
|
||||
},
|
||||
watch: {
|
||||
// 图表类型切换时重新绘制
|
||||
chartType: {
|
||||
handler: 'drawChart',
|
||||
immediate: true
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
// 1. 初始化ECharts实例
|
||||
initChart() {
|
||||
this.chartInstance = echarts.init(this.$refs.chartRef)
|
||||
this.drawChart()
|
||||
},
|
||||
|
||||
// 2. 绘制图表(核心:从tableData提取数据)
|
||||
drawChart() {
|
||||
// 从统一数据源tableData提取图表所需数据
|
||||
const xData = this.tableData.map(item => item.periodTime) // X轴:月份
|
||||
const hasValidData = this.tableData.some(item => item.currentEnergy !== null) // 是否有有效数据
|
||||
|
||||
// 处理数据:null值转为0(避免图表报错)
|
||||
const currentData = this.tableData.map(item => item.currentEnergy ?? 0)
|
||||
const samePeriodData = this.tableData.map(item => item.samePeriodEnergy ?? 0)
|
||||
|
||||
const chartOption = {
|
||||
// 提示框配置(显示同比信息)
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: { type: this.chartType === 'bar' ? 'shadow' : 'line' },
|
||||
formatter: (params) => {
|
||||
const month = params[0].name
|
||||
const currentItem = this.tableData.find(item => item.periodTime === month)
|
||||
let tooltipHtml = `<div>${month}</div>`
|
||||
|
||||
params.forEach(param => {
|
||||
const seriesName = param.seriesName
|
||||
const value = param.value
|
||||
const percent = currentItem.yearOnYear !== null
|
||||
? `(同比:${(currentItem.yearOnYear * 100).toFixed(2)}%)`
|
||||
: ''
|
||||
tooltipHtml += `<div>${seriesName}:${value} ${this.unit} ${percent}</div>`
|
||||
})
|
||||
return tooltipHtml
|
||||
}
|
||||
},
|
||||
|
||||
// 图表网格(边距调整)
|
||||
grid: {
|
||||
left: '3%',
|
||||
right: '4%',
|
||||
bottom: '3%',
|
||||
containLabel: true
|
||||
},
|
||||
|
||||
// X轴配置
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: xData,
|
||||
axisLabel: { interval: 0 } // 强制显示所有月份标签
|
||||
},
|
||||
|
||||
// Y轴配置(显示能耗单位)
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
name: `能耗(${this.unit})`,
|
||||
nameTextStyle: { marginRight: 10 }
|
||||
},
|
||||
|
||||
// 系列数据(本期+同期)
|
||||
series: [
|
||||
{
|
||||
name: `本期(${this.year}年)`,
|
||||
type: this.chartType,
|
||||
data: hasValidData ? currentData : [], // 无数据时传空数组,触发无数据提示
|
||||
itemStyle: { color: '#409EFF' },
|
||||
...(this.chartType === 'bar' && { barBorderRadius: [4, 4, 0, 0] }) // 柱状图圆角
|
||||
},
|
||||
{
|
||||
name: `同期(${Number(this.year) - 1}年)`,
|
||||
type: this.chartType,
|
||||
data: hasValidData ? samePeriodData : [],
|
||||
itemStyle: { color: '#C084FC' },
|
||||
...(this.chartType === 'bar' && { barBorderRadius: [4, 4, 0, 0] })
|
||||
}
|
||||
],
|
||||
|
||||
// 无数据提示配置
|
||||
noDataLoadingOption: {
|
||||
text: '暂无数据,请选择年份并点击查询',
|
||||
textStyle: { fontSize: 16, color: '#999' },
|
||||
position: 'center'
|
||||
}
|
||||
}
|
||||
|
||||
this.chartInstance.setOption(chartOption)
|
||||
},
|
||||
|
||||
// 3. 处理查询(核心:调用模拟API,同步更新tableData)
|
||||
handleQuery() {
|
||||
// 校验年份(必须是4位数字)
|
||||
console.log(this.year)
|
||||
if (!/^\d{4}$/.test(this.year)) {
|
||||
this.$message.warning('请输入有效的4位年份(如2025)')
|
||||
return
|
||||
}
|
||||
|
||||
// 模拟API请求(实际项目替换为axios请求)
|
||||
this.mockGetEnergyData(this.year)
|
||||
.then(apiData => {
|
||||
// 同步更新统一数据源:表格和图表将自动使用新数据
|
||||
this.tableData = apiData
|
||||
// 重新绘制图表(确保图表使用新数据)
|
||||
this.drawChart()
|
||||
this.$message.success(`成功获取${this.year}年能耗数据`)
|
||||
})
|
||||
.catch(err => {
|
||||
this.$message.error(`数据获取失败:${err.message}`)
|
||||
})
|
||||
},
|
||||
|
||||
// 4. 刷新数据(重新调用查询逻辑)
|
||||
handleRefresh() {
|
||||
this.handleQuery()
|
||||
},
|
||||
|
||||
// 5. 窗口大小变化时调整图表尺寸
|
||||
handleResize() {
|
||||
this.chartInstance?.resize()
|
||||
},
|
||||
|
||||
// 6. 模拟API:根据年份返回能耗数据(实际项目替换为后端接口)
|
||||
mockGetEnergyData(year) {
|
||||
return new Promise((resolve) => {
|
||||
// 模拟网络延迟(800ms)
|
||||
setTimeout(() => {
|
||||
const currentYear = Number(year)
|
||||
const lastYear = currentYear - 1
|
||||
|
||||
// 生成12个月的模拟数据(确保表格和图表数据一致)
|
||||
const mockData = Array.from({ length: 12 }, (_, index) => {
|
||||
const month = index + 1
|
||||
const periodTime = `${month}月`
|
||||
|
||||
// 生成合理的随机能耗(根据年份区分基数,避免数据重复)
|
||||
const currentEnergy = Math.floor(currentYear * 5 + Math.random() * 100) // 本期能耗
|
||||
const samePeriodEnergy = Math.floor(lastYear * 5 + Math.random() * 100) // 同期能耗
|
||||
|
||||
// 计算同比(处理同期为0的异常情况)
|
||||
const yearOnYear = samePeriodEnergy === 0
|
||||
? (currentEnergy > 0 ? 1 : 0) // 同期为0时,本期有值则同比100%
|
||||
: (currentEnergy - samePeriodEnergy) / samePeriodEnergy
|
||||
|
||||
return {
|
||||
periodTime,
|
||||
currentEnergy,
|
||||
samePeriodEnergy,
|
||||
yearOnYear // 保留小数(表格显示时转为百分比)
|
||||
}
|
||||
})
|
||||
|
||||
resolve(mockData)
|
||||
}, 800)
|
||||
})
|
||||
},
|
||||
|
||||
refresh() {
|
||||
this.handleQuery()
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.energy-analysis-container {
|
||||
padding: 20px;
|
||||
background-color: #fff;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.query-section {
|
||||
margin-bottom: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.chart-section {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.echarts-box {
|
||||
width: 100%;
|
||||
height: 400px;
|
||||
border: 1px solid #e6e6e6;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
/* 图例样式 */
|
||||
.legend-item {
|
||||
display: inline-block;
|
||||
margin-right: 20px;
|
||||
vertical-align: middle;
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.legend-dot {
|
||||
display: inline-block;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
margin-right: 5px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
/* 图表操作区(右对齐) */
|
||||
.chart-operation {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
/* 图表类型按钮激活状态 */
|
||||
.el-button-group .active {
|
||||
color: #409EFF;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* 表格同比颜色(红色:上升,绿色:下降) */
|
||||
.text-red {
|
||||
color: #F56C6C;
|
||||
}
|
||||
|
||||
.text-green {
|
||||
color: #67C23A;
|
||||
}
|
||||
|
||||
.table-section {
|
||||
margin-top: 20px;
|
||||
}
|
||||
</style>
|
||||
@@ -22,6 +22,10 @@
|
||||
<el-button icon="el-icon-download" @click="downloadTemplate">下载模板</el-button>
|
||||
<el-button icon="el-icon-upload" @click="handleImport">导入设备</el-button>
|
||||
<el-button icon="el-icon-search" @click="showSearch = !showSearch">{{ showSearch ? '隐藏' : '显示' }}搜索</el-button>
|
||||
<el-button-group style="margin-left: auto;">
|
||||
<el-button :type="viewType === 'card' ? 'primary' : ''" @click="viewType = 'card'">卡片视图</el-button>
|
||||
<el-button :type="viewType === 'table' ? 'primary' : ''" @click="viewType = 'table'">表格视图</el-button>
|
||||
</el-button-group>
|
||||
<input
|
||||
ref="importFile"
|
||||
type="file"
|
||||
@@ -57,68 +61,121 @@
|
||||
</div>
|
||||
</el-collapse-transition>
|
||||
|
||||
<!-- 设备卡片列表 -->
|
||||
<!-- 设备卡片/表格列表 -->
|
||||
<div class="devices-container" v-loading="loading">
|
||||
<div v-if="meterList.length === 0" class="empty-state">
|
||||
<div class="empty-icon">📦</div>
|
||||
<div class="empty-text">暂无设备数据</div>
|
||||
</div>
|
||||
<el-row :gutter="20" class="device-cards" v-else>
|
||||
<el-col
|
||||
v-for="meter in meterList"
|
||||
:key="meter.meterId"
|
||||
:xs="24" :sm="12" :md="8" :lg="6"
|
||||
>
|
||||
<div class="device-card">
|
||||
<!-- 卡片头部 -->
|
||||
<div class="card-header">
|
||||
<div class="header-left">
|
||||
<div class="meter-code">{{ meter.meterCode }}</div>
|
||||
<div class="energy-type">{{ getEnergyName(meter.energyTypeId) }}</div>
|
||||
</div>
|
||||
<el-tag
|
||||
:type="getStatusType(meter.status)"
|
||||
size="small"
|
||||
>
|
||||
{{ getStatusText(meter.status) }}
|
||||
</el-tag>
|
||||
</div>
|
||||
|
||||
<!-- 卡片主体 -->
|
||||
<div class="card-body">
|
||||
<div class="info-item">
|
||||
<span class="label">型号</span>
|
||||
<span class="value">{{ meter.model || '-' }}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="label">制造商</span>
|
||||
<span class="value">{{ meter.manufacturer || '-' }}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="label">安装日期</span>
|
||||
<span class="value">{{ meter.installDate || '-' }}</span>
|
||||
</div>
|
||||
<div class="info-item status-selector">
|
||||
<el-radio-group
|
||||
v-model="meter.status"
|
||||
<!-- 卡片视图 -->
|
||||
<template v-if="viewType === 'card'">
|
||||
<div v-if="meterList.length === 0" class="empty-state">
|
||||
<div class="empty-icon">📦</div>
|
||||
<div class="empty-text">暂无设备数据</div>
|
||||
</div>
|
||||
<el-row :gutter="20" class="device-cards" v-else>
|
||||
<el-col
|
||||
v-for="meter in meterList"
|
||||
:key="meter.meterId"
|
||||
:xs="24" :sm="12" :md="8" :lg="6"
|
||||
>
|
||||
<div class="device-card">
|
||||
<!-- 卡片头部 -->
|
||||
<div class="card-header">
|
||||
<div class="header-left">
|
||||
<div class="meter-code">{{ meter.meterCode }}</div>
|
||||
<div class="energy-type">{{ getEnergyName(meter.energyTypeId) }}[{{ meter.productionLine || '-' }}]</div>
|
||||
</div>
|
||||
<el-tag
|
||||
:type="getStatusType(meter.status)"
|
||||
size="small"
|
||||
@input="handleStatusChange(meter)"
|
||||
>
|
||||
<el-radio-button :label="0">在用</el-radio-button>
|
||||
<el-radio-button :label="1">停用</el-radio-button>
|
||||
<el-radio-button :label="2">维护</el-radio-button>
|
||||
</el-radio-group>
|
||||
{{ getStatusText(meter.status) }}
|
||||
</el-tag>
|
||||
</div>
|
||||
|
||||
<!-- 卡片主体 -->
|
||||
<div class="card-body">
|
||||
<div class="info-item">
|
||||
<span class="label">型号</span>
|
||||
<span class="value">{{ meter.model || '-' }}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="label">制造商</span>
|
||||
<span class="value">{{ meter.manufacturer || '-' }}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="label">安装日期</span>
|
||||
<span class="value">{{ meter.installDate || '-' }}</span>
|
||||
</div>
|
||||
<div class="info-item status-selector">
|
||||
<el-radio-group
|
||||
v-model="meter.status"
|
||||
size="small"
|
||||
@input="handleStatusChange(meter)"
|
||||
>
|
||||
<el-radio-button :label="0">在用</el-radio-button>
|
||||
<el-radio-button :label="1">停用</el-radio-button>
|
||||
<el-radio-button :label="2">维护</el-radio-button>
|
||||
</el-radio-group>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 卡片底部操作 -->
|
||||
<div class="card-footer">
|
||||
<el-button type="primary" size="small" @click="handleEditMeter(meter)">编辑</el-button>
|
||||
<el-button type="danger" size="small" @click="handleDeleteMeter(meter)">删除</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 卡片底部操作 -->
|
||||
<div class="card-footer">
|
||||
<el-button type="primary" size="small" @click="handleEditMeter(meter)">编辑</el-button>
|
||||
<el-button type="danger" size="small" @click="handleDeleteMeter(meter)">删除</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</template>
|
||||
|
||||
<!-- 表格视图 -->
|
||||
<template v-else>
|
||||
<el-table v-loading="loading" :data="meterList" border>
|
||||
<el-table-column label="设备名称" align="center" prop="meterCode" />
|
||||
<el-table-column label="能源类型" align="center" width="150">
|
||||
<template slot-scope="scope">
|
||||
{{ getEnergyName(scope.row.energyTypeId) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="相关产线" align="center" prop="productionLine" />
|
||||
<el-table-column label="型号" align="center" prop="model" />
|
||||
<el-table-column label="制造商" align="center" prop="manufacturer" />
|
||||
<el-table-column label="安装日期" align="center" prop="installDate" />
|
||||
<el-table-column label="状态" align="center" width="150">
|
||||
<template slot-scope="scope">
|
||||
<el-tag :type="getStatusType(scope.row.status)" size="small">
|
||||
{{ getStatusText(scope.row.status) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态选择" align="center" width="220">
|
||||
<template slot-scope="scope">
|
||||
<el-radio-group
|
||||
v-model="scope.row.status"
|
||||
size="small"
|
||||
@input="handleStatusChange(scope.row)"
|
||||
>
|
||||
<el-radio-button :label="0">在用</el-radio-button>
|
||||
<el-radio-button :label="1">停用</el-radio-button>
|
||||
<el-radio-button :label="2">维护</el-radio-button>
|
||||
</el-radio-group>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" align="center" width="150">
|
||||
<template slot-scope="scope">
|
||||
<el-button
|
||||
size="mini"
|
||||
type="primary"
|
||||
@click="handleEditMeter(scope.row)"
|
||||
>编辑</el-button>
|
||||
<el-button
|
||||
size="mini"
|
||||
type="danger"
|
||||
@click="handleDeleteMeter(scope.row)"
|
||||
>删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- 新增/编辑设备对话框 -->
|
||||
@@ -143,9 +200,8 @@
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="相关产线" prop="lineId">
|
||||
<el-select v-model="meterForm.productionLine" placeholder="请选择相关产线" filterable clearable>
|
||||
<el-option v-for="item in dict.type.sys_lines" :key="item.value" :label="item.label" :value="item.value" />
|
||||
</el-select>
|
||||
<muti-select v-model="meterForm.productionLine" :options="dict.type.sys_lines" placeholder="请选择相关产线" filterable clearable>
|
||||
</muti-select>
|
||||
</el-form-item>
|
||||
|
||||
<!-- 基本信息 -->
|
||||
@@ -185,6 +241,7 @@ export default {
|
||||
loading: false,
|
||||
importLoading: false,
|
||||
showSearch: false,
|
||||
viewType: 'card', // 'card' or 'table'
|
||||
meterList: [],
|
||||
energyTypeList: [],
|
||||
warehouseList: [],
|
||||
@@ -694,8 +751,6 @@ export default {
|
||||
flex: 1;
|
||||
padding: 6px 0 !important;
|
||||
font-size: 12px !important;
|
||||
|
||||
|
||||
}
|
||||
|
||||
// 能源类型卡片选择
|
||||
|
||||
@@ -42,7 +42,7 @@
|
||||
<el-table :data="tableData" style="width: 100%" border :show-summary="configOptions.showSumRow" :summary-method="getSummaries">
|
||||
<el-table-column prop="date" label="日期" width="120" />
|
||||
<el-table-column v-for="meter in meters" :key="meter.meterId" :prop="`meter_${meter.meterId}`"
|
||||
:label="meter.meterCode">
|
||||
:label="meter.meterCode + '(' + (meter.productionLine || '暂无') + ')'">
|
||||
<template slot-scope="scope">
|
||||
<input v-model="scope.row[`meter_${meter.meterId}`]" class="nob"
|
||||
@change="(e) => handleCellChange(scope.row, meter.meterId, e)" />
|
||||
|
||||
Reference in New Issue
Block a user