feat: 图表

This commit is contained in:
砂糖
2025-09-30 10:01:16 +08:00
parent 6c483ad6f5
commit 8b31f9bf84
13 changed files with 1287 additions and 142 deletions

View File

@@ -0,0 +1,19 @@
import request from '@/utils/request'
// 近期能耗情况汇总
export function getRecentEnergySummary(query) {
return request({
url: '/ems/energyConsumption/getEnergySummary',
method: 'get',
params: query
})
}
// 近期能耗趋势
export function getRecentEnergyTrend(query) {
return request({
url: '/ems/energyConsumption/getEnergyTrend',
method: 'get',
params: query
})
}

View File

@@ -1051,7 +1051,7 @@ body {
// background-color: $--color-background-light;
border-radius: 6px 6px 0 0;
padding: 0 var(--spacing-base);
box-shadow: inset 0 -1px 0 $--border-color-light;
// box-shadow: inset 0 -1px 0 $--border-color-light;
}
// 标签项
@@ -1128,11 +1128,10 @@ body {
// 标签内容区
.el-tabs__content {
padding: var(--spacing-lg);
// padding: var(--spacing-lg);
background-color: $--metal-gradient-light;
border: 1px solid $--border-color-light;
border-radius: 0 0 6px 6px;
min-height: 100px; // 确保有基本高度
// border: 1px solid $--border-color-light;
// border-radius: 0 0 6px 6px;
}
// 卡片类型标签页
@@ -1150,14 +1149,15 @@ body {
.el-tabs__item {
border: 1px solid $--border-color-light;
border-bottom: none;
border-radius: 6px 6px 0 0;
// border-radius: 6px 6px 0 0;
margin: 0 2px;
background-color: $--metal-gradient-light;
&.is-active {
border-color: $--border-color-light;
border-bottom-color: $--metal-gradient-light;
background-color: $--metal-gradient-light;
background-color: $--color-primary;
color: $--metal-gradient-light;
position: relative;
bottom: -1px;
}

View File

@@ -53,8 +53,5 @@ export default {
return !this.sidebar.opened;
}
},
mounted() {
console.log(this.sidebarRouters, sidebar);
}
};
</script>

View File

@@ -104,7 +104,7 @@ export default {
filteredMenus() {
const filterHidden = (menus) => {
return menus
.filter(menu => menu.hidden!== true)
.filter(menu => menu.hidden !== true)
.map(menu => {
if (menu.children) {
menu.children = filterHidden(menu.children)

View File

@@ -1,9 +1,12 @@
<template>
<el-row>
<el-col :span="4">
<el-select v-model="energyType" placeholder="请选择能源类型">
<el-option v-for="item in energyTypeList" :key="item.energyTypeId" :label="item.name" :value="item.energyTypeId" />
</el-select>
<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>
<!-- 区域选择默认展开最高高度为60%的屏幕尺寸 -->
<el-tree @node-click="handleNodeClick" :data="locationList" :props="defaultProps" :default-expand-all="true" :style="{ height: 'calc(50vh - 50px)' }"></el-tree>
@@ -17,19 +20,21 @@
</div>
</el-col>
<el-col :span="20">
<el-tabs v-model="activeTab">
<el-tab-pane label="环比概况" name="1">
</el-tab-pane>
<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>
<Overview v-if="activeTab === '1'" />
<RecentTrend v-if="activeTab === '2'" />
<YearToYear v-if="activeTab === '3'" />
<MonthToMonth v-if="activeTab === '4'" />
<Overview ref="overview" v-if="activeTab === '1'" :unit="energyUnit" :energyName="energyName" :energyType="energyType" :locationId="locationId" :deviceId="deviceId" />
<RecentTrend ref="recentTrend" v-if="activeTab === '2'" :unit="energyUnit" :energyName="energyName" :energyType="energyType" :locationId="locationId" :deviceId="deviceId" />
<YearToYear ref="yearToYear" v-if="activeTab === '3'" :unit="energyUnit" :energyName="energyName" :energyType="energyType" :locationId="locationId" :deviceId="deviceId" />
<MonthToMonth ref="monthToMonth" v-if="activeTab === '4'" :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>
</template>
@@ -39,10 +44,9 @@ import { listEnergyType } from "@/api/ems/energyType";
import { listLocation } from "@/api/ems/location";
import Overview from "./panels/Overview.vue";
import RecentTrend from "./panels/RecentTrend.vue";
import YearToYear from "./panels/YearToYear.vue";
import RecentTrend from "./panels/RecentTrends.vue";
import YearToYear from "./panels/YearOnYear.vue";
import MonthToMonth from "./panels/MonthToMonth.vue";
import Overview from "./panels/Overview.vue";
export default {
name: "Dashboard",
@@ -57,6 +61,7 @@ export default {
energyTypeList: [],
energyType: '',
locationId: '',
deviceId: '',
locationList: [
],
defaultProps: {
@@ -70,11 +75,16 @@ export default {
this.getEnergyTypeList();
this.getLocationList();
},
watch: {
// 当选择的区域变化时重新获取设备列表
locationId: {
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;
}
},
methods: {
getEnergyTypeList() {
@@ -84,13 +94,27 @@ export default {
},
getLocationList() {
listLocation().then(response => {
const data = { locationId: undefined, name: '顶级节点', children: [] };
data.children = this.handleTree(response.data, "locationId", "parentId");
this.locationList.push(data);
this.locationList = this.handleTree(response.data, "locationId", "parentId");
});
},
handleNodeClick(data) {
this.locationId = data.locationId;
this.deviceId = undefined;
this.refresh();
},
refresh() {
if (this.$refs.overview) {
this.$refs.overview.refresh();
}
if (this.$refs.recentTrend) {
this.$refs.recentTrend.refresh();
}
if (this.$refs.yearToYear) {
this.$refs.yearToYear.refresh();
}
if (this.$refs.monthToMonth) {
this.$refs.monthToMonth.refresh();
}
}
}
}

View File

@@ -0,0 +1,239 @@
<template>
<div class="energy-analysis-container">
<!-- 年份选择与查询按钮区域 -->
<div class="query-section">
<el-input
v-model="year"
placeholder="请输入年份"
style="width: 200px; 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>
<div class="legend-item">
<span class="legend-dot" style="background-color: #409EFF;"></span>
<span>本期</span>
</div>
<div class="legend-item">
<span class="legend-dot" style="background-color: #C084FC;"></span>
<span>同期</span>
</div>
</el-col>
<!-- 图表类型切换 + 刷新 -->
<el-col :offset="18">
<el-button-group>
<el-button type="text" @click="chartType = 'bar'">柱状图</el-button>
<el-button type="text" @click="chartType = 'line'">折线图</el-button>
</el-button-group>
<el-button icon="el-icon-refresh" type="text" @click="handleRefresh">刷新</el-button>
</el-col>
</el-row>
<!-- ECharts 容器 -->
<div ref="chartRef" class="echarts-box" />
</div>
<!-- 同比分析表格 -->
<div class="table-section">
<el-table :data="tableData" border style="width: 100%">
<el-table-column prop="periodTime" label="本期时间" />
<el-table-column prop="currentEnergy" label="本期能耗(Nm3)" />
<el-table-column prop="samePeriodEnergy" label="同比能耗(Nm3)" />
<el-table-column prop="yearOnYear" label="同比(%)" />
</el-table>
</div>
</div>
</template>
<script>
import echarts from 'echarts'
export default {
name: 'MonthToMonthAnalysis',
props: {
energyType: {
type: String,
default: ''
},
locationId: {
type: String,
default: ''
},
deviceId: {
type: String,
default: ''
},
unit: {
type: String,
default: ''
},
energyName: {
type: String,
default: ''
}
},
data() {
return {
year: '2025',
chartType: 'bar',
tableData: [
{ periodTime: '1月', currentEnergy: '--', samePeriodEnergy: '--', yearOnYear: '--' },
{ periodTime: '2月', currentEnergy: '--', samePeriodEnergy: '--', yearOnYear: '--' },
{ periodTime: '3月', currentEnergy: '--', samePeriodEnergy: '--', yearOnYear: '--' },
{ periodTime: '4月', currentEnergy: '--', samePeriodEnergy: '--', yearOnYear: '--' },
{ periodTime: '5月', currentEnergy: '--', samePeriodEnergy: '--', yearOnYear: '--' },
{ periodTime: '6月', currentEnergy: '--', samePeriodEnergy: '--', yearOnYear: '--' },
{ periodTime: '7月', currentEnergy: '--', samePeriodEnergy: '--', yearOnYear: '--' },
{ periodTime: '8月', currentEnergy: '--', samePeriodEnergy: '--', yearOnYear: '--' },
{ periodTime: '9月', currentEnergy: '--', samePeriodEnergy: '--', yearOnYear: '--' },
{ periodTime: '10月', currentEnergy: '--', samePeriodEnergy: '--', yearOnYear: '--' },
{ periodTime: '11月', currentEnergy: '--', samePeriodEnergy: '--', yearOnYear: '--' },
{ periodTime: '12月', currentEnergy: '--', samePeriodEnergy: '--', yearOnYear: '--' }
],
chartInstance: null
}
},
mounted() {
this.initChart()
},
beforeDestroy() {
if (this.chartInstance) {
this.chartInstance.dispose()
}
},
watch: {
chartType: {
handler(newVal) {
this.drawChart()
},
immediate: true
}
},
methods: {
// 初始化图表
initChart() {
this.chartInstance = echarts.init(this.$refs.chartRef)
this.drawChart()
// 监听窗口大小变化,调整图表尺寸
window.addEventListener('resize', this.handleResize)
},
// 绘制图表
drawChart() {
const option = {
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow'
}
},
grid: {
left: '3%',
right: '4%',
bottom: '3%',
containLabel: true
},
xAxis: {
type: 'category',
data: ['1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月']
},
yAxis: {
type: 'value'
},
series: [
{
name: '本期',
type: this.chartType,
data: [120, 132, 101, 134, 90, 230, 210, 120, 132, 101, 134, 90],
itemStyle: {
color: '#409EFF'
}
},
{
name: '同期',
type: this.chartType,
data: [90, 110, 120, 110, 120, 140, 130, 90, 110, 120, 110, 120],
itemStyle: {
color: '#C084FC'
}
}
]
}
this.chartInstance.setOption(option)
},
// 处理查询
handleQuery() {
// 实际项目中这里会调用接口获取数据
console.log('查询年份:', this.year)
// 模拟数据加载
this.tableData = this.tableData.map(item => ({
...item,
currentEnergy: Math.floor(Math.random() * 1000) + '',
samePeriodEnergy: Math.floor(Math.random() * 1000) + '',
yearOnYear: (Math.random() * 20 - 10).toFixed(2) + ''
}))
this.drawChart()
},
// 刷新图表
handleRefresh() {
this.drawChart()
},
// 处理窗口大小变化
handleResize() {
if (this.chartInstance) {
this.chartInstance.resize()
}
}
}
}
</script>
<style scoped>
.energy-analysis-container {
padding: 20px;
background-color: #fff;
border-radius: 4px;
}
.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;
}
.legend-dot {
display: inline-block;
width: 12px;
height: 12px;
border-radius: 50%;
margin-right: 5px;
}
.table-section {
margin-top: 20px;
}
</style>

View File

@@ -0,0 +1,320 @@
<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>

View File

@@ -0,0 +1,382 @@
<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>

View File

@@ -0,0 +1,5 @@
<template>
<div>
同比分析
</div>
</template>

View File

@@ -114,12 +114,12 @@
<el-table-column label="消耗量" align="center" prop="consumption" />
<el-table-column label="起始时间" align="center" prop="startTime" width="180">
<template slot-scope="scope">
<span>{{ parseTime(scope.row.startTime, '{y}-{m}-{d}') }}</span>
<span>{{ parseTime(scope.row.startTime, '{y}-{m}-{d} {h}:{i}:{s}') }}</span>
</template>
</el-table-column>
<el-table-column label="结束时间" align="center" prop="endTime" width="180">
<template slot-scope="scope">
<span>{{ parseTime(scope.row.endTime, '{y}-{m}-{d}') }}</span>
<span>{{ parseTime(scope.row.endTime, '{y}-{m}-{d} {h}:{i}:{s}') }}</span>
</template>
</el-table-column>
<el-table-column label="记录人" align="center" prop="recordedBy" />

View File

@@ -68,85 +68,82 @@
icon="el-icon-plus"
size="mini"
@click="handleAdd"
>新增</el-button>
</el-col>
<el-col :span="1.5">
<el-button
type="success"
plain
icon="el-icon-edit"
size="mini"
:disabled="single"
@click="handleUpdate"
>修改</el-button>
</el-col>
<el-col :span="1.5">
<el-button
type="danger"
plain
icon="el-icon-delete"
size="mini"
:disabled="multiple"
@click="handleDelete"
>删除</el-button>
</el-col>
<el-col :span="1.5">
<el-button
type="warning"
plain
icon="el-icon-download"
size="mini"
@click="handleExport"
>导出</el-button>
>新增设备</el-button>
</el-col>
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
<el-table v-loading="loading" :data="meterList" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="设备编号" align="center" prop="meterCode" />
<el-table-column label="能源类型" align="center" prop="energyTypeId">
<template slot-scope="scope">
<span>{{ getEnergy(scope.row.energyTypeId).name }}</span>
</template>
</el-table-column>
<el-table-column label="安装位置" align="center" prop="locationId">
<template slot-scope="scope">
<span>{{ getLocationName(scope.row.locationId) }}</span>
</template>
</el-table-column>
<el-table-column label="设备型号" align="center" prop="model" />
<el-table-column label="制造商" align="center" prop="manufacturer" />
<el-table-column label="安装日期" align="center" prop="installDate" width="180">
<template slot-scope="scope">
<span>{{ parseTime(scope.row.installDate, '{y}-{m}-{d}') }}</span>
</template>
</el-table-column>
<el-table-column label="设备状态" align="center" prop="status" />
<el-table-column label="校准日期" align="center" prop="lastCalibrationDate" width="180">
<template slot-scope="scope">
<span>{{ parseTime(scope.row.lastCalibrationDate, '{y}-{m}-{d}') }}</span>
</template>
</el-table-column>
<el-table-column label="阈值" align="center" prop="thresholdValue" />
<el-table-column label="备注" align="center" prop="remark" />
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
<template slot-scope="scope">
<el-button
size="mini"
type="text"
icon="el-icon-edit"
@click="handleUpdate(scope.row)"
>修改</el-button>
<el-button
size="mini"
type="text"
icon="el-icon-delete"
@click="handleDelete(scope.row)"
>删除</el-button>
</template>
</el-table-column>
</el-table>
<!-- 卡片布局 -->
<div v-loading="loading" class="card-container">
<el-row :gutter="20">
<el-col
v-for="item in meterList"
:key="item.meterId"
:xs="24"
:sm="12"
:md="6"
:lg="4"
class="card-col"
>
<el-card class="meter-card" shadow="hover">
<div class="card-header">
<div class="device-code">{{ item.meterCode }}</div>
<div class="device-status">
{{ getEnergy(item.energyTypeId).name }}
</div>
</div>
<div class="card-content">
<div class="card-item">
<span class="item-label">安装位置:</span>
<span class="item-value">{{ getLocationName(item.locationId) }}</span>
</div>
<div class="card-item">
<span class="item-label">设备型号:</span>
<span class="item-value">{{ item.model }}</span>
</div>
<div class="card-item">
<span class="item-label">制造商:</span>
<span class="item-value">{{ item.manufacturer }}</span>
</div>
<div class="card-item">
<span class="item-label">安装日期:</span>
<span class="item-value">{{ parseTime(item.installDate, '{y}-{m}-{d}') }}</span>
</div>
<div class="card-item">
<span class="item-label">校准日期:</span>
<span class="item-value">{{ parseTime(item.lastCalibrationDate, '{y}-{m}-{d}') }}</span>
</div>
<div class="card-item">
<span class="item-label">阈值:</span>
<span class="item-value">{{ item.thresholdValue }}</span>
</div>
<div class="card-item">
<span class="item-label">备注:</span>
<span class="item-value">{{ item.remark || '无' }}</span>
</div>
</div>
<div class="card-actions">
<el-button
size="mini"
type="primary"
icon="el-icon-edit"
@click="handleUpdate(item)"
plain
>编辑</el-button>
<el-button
size="mini"
type="danger"
icon="el-icon-delete"
@click="handleDelete(item)"
plain
>删除</el-button>
</div>
</el-card>
</el-col>
</el-row>
</div>
<pagination
v-show="total>0"
@@ -156,7 +153,7 @@
@pagination="getList"
/>
<!-- 添加或修改计量设备阈值移至此处对话框 -->
<!-- 添加或修改计量设备对话框 -->
<el-dialog :title="title" :visible.sync="open" width="500px" append-to-body>
<el-form ref="form" :model="form" :rules="rules" label-width="80px">
<el-form-item label="设备编号" prop="meterCode">
@@ -168,8 +165,7 @@
</el-select>
</el-form-item>
<el-form-item label="安装位置" prop="locationId">
<!-- <el-input v-model="form.locationId" placeholder="请输入安装位置" /> -->
<treeselect v-model="form.locationId" :options="locationOptions" :normalizer="normalizer" placeholder="请选择安装位置" />
<treeselect v-model="form.locationId" :options="locationOptions" :normalizer="normalizer" placeholder="请选择安装位置" />
</el-form-item>
<el-form-item label="设备型号" prop="model">
<el-input v-model="form.model" placeholder="请输入设备型号" />
@@ -229,17 +225,11 @@ export default {
buttonLoading: false,
// 遮罩层
loading: true,
// 选中数组
ids: [],
// 非单个禁用
single: true,
// 非多个禁用
multiple: true,
// 显示搜索条件
showSearch: true,
// 总条数
total: 0,
// 计量设备(阈值移至此处)表格数据
// 计量设备表格数据
meterList: [],
// 弹出层标题
title: "",
@@ -263,6 +253,24 @@ export default {
form: {},
// 表单校验
rules: {
meterCode: [
{ required: true, message: "设备编号不能为空", trigger: "blur" }
],
energyTypeId: [
{ required: true, message: "能源类型不能为空", trigger: "change" }
],
locationId: [
{ required: true, message: "安装位置不能为空", trigger: "change" }
],
model: [
{ required: true, message: "设备型号不能为空", trigger: "blur" }
],
installDate: [
{ required: true, message: "安装日期不能为空", trigger: "change" }
],
thresholdValue: [
{ required: true, message: "阈值不能为空", trigger: "blur" }
]
}
};
},
@@ -272,7 +280,7 @@ export default {
this.getEnergyTypeList();
},
methods: {
/** 查询计量设备(阈值移至此处)列表 */
/** 查询计量设备列表 */
getList() {
this.loading = true;
listMeter(this.queryParams).then(response => {
@@ -342,28 +350,22 @@ export default {
this.resetForm("queryForm");
this.handleQuery();
},
// 多选框选中数据
handleSelectionChange(selection) {
this.ids = selection.map(item => item.meterId)
this.single = selection.length!==1
this.multiple = !selection.length
},
/** 新增按钮操作 */
handleAdd() {
this.reset();
this.open = true;
this.title = "添加计量设备(阈值移至此处)";
this.title = "添加计量设备";
},
/** 修改按钮操作 */
handleUpdate(row) {
this.loading = true;
this.reset();
const meterId = row.meterId || this.ids
const meterId = row.meterId;
getMeter(meterId).then(response => {
this.loading = false;
this.form = response.data;
this.open = true;
this.title = "修改计量设备(阈值移至此处)";
this.title = "修改计量设备";
});
},
/** 提交按钮 */
@@ -392,17 +394,38 @@ export default {
});
},
getLocationName(id) {
return this.locationList.find(item => item.locationId === id).name ?? '';
const location = this.locationList.find(item => item.locationId === id);
return location ? location.name : '';
},
getEnergy(id) {
return this.energyTypeList.find(item => item.energyTypeId === id) ?? {};
const energy = this.energyTypeList.find(item => item.energyTypeId === id);
return energy || { name: '' };
},
getStatusClass(status) {
if (status === '正常') return 'status-normal';
if (status === '故障') return 'status-fault';
if (status === '维护中') return 'status-maintenance';
if (status === '离线') return 'status-offline';
return '';
},
parseTime(time, format) {
if (!time) return '';
const date = new Date(time);
const year = date.getFullYear();
const month = (date.getMonth() + 1).toString().padStart(2, '0');
const day = date.getDate().toString().padStart(2, '0');
if (format === '{y}-{m}-{d}') {
return `${year}-${month}-${day}`;
}
return `${year}-${month}-${day}`;
},
/** 删除按钮操作 */
handleDelete(row) {
const meterIds = row.meterId || this.ids;
this.$modal.confirm('是否确认删除计量设备(阈值移至此处)编号为"' + meterIds + '"的数据项?').then(() => {
const meterId = row.meterId;
this.$modal.confirm('是否确认删除计量设备编号为"' + row.meterCode + '"的数据项?').then(() => {
this.loading = true;
return delMeter(meterIds);
return delMeter(meterId);
}).then(() => {
this.loading = false;
this.getList();
@@ -411,13 +434,145 @@ export default {
}).finally(() => {
this.loading = false;
});
},
/** 导出按钮操作 */
handleExport() {
this.download('ems/meter/export', {
...this.queryParams
}, `meter_${new Date().getTime()}.xlsx`)
}
}
};
</script>
<style scoped>
.app-container {
padding: 20px;
background-color: #f5f7fa;
}
.card-container {
margin-top: 20px;
}
.card-col {
margin-bottom: 20px;
}
.meter-card {
height: 100%;
border-radius: 0px;
transition: all 0.3s ease;
border: none;
overflow: hidden;
}
.meter-card:hover {
transform: translateY(-5px);
/* box-shadow: 0 10px 20px rgba(0, 0, 0, 0.12); */
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
padding-bottom: 5px;
border-bottom: 1px solid #eee;
margin-bottom: 5px;
}
.device-code {
font-weight: bold;
font-size: 18px;
color: #1a73e8;
}
.device-status {
padding: 4px 10px;
border-radius: 15px;
font-size: 12px;
font-weight: bold;
}
.status-normal {
background-color: #e8f5e9;
color: #2e7d32;
}
.status-fault {
background-color: #ffebee;
color: #c62828;
}
.status-maintenance {
background-color: #fff8e1;
color: #f57f17;
}
.status-offline {
background-color: #f5f5f5;
color: #757575;
}
.card-content {
padding: 5px 0;
}
.card-item {
display: flex;
margin-bottom: 5px;
line-height: 1.5;
font-size: 14px;
}
.item-label {
font-weight: bold;
min-width: 80px;
color: #666;
}
.item-value {
flex-grow: 1;
word-break: break-word;
color: #333;
}
.card-actions {
display: flex;
justify-content: flex-end;
padding-top: 15px;
margin-top: 10px;
border-top: 1px solid #eee;
}
.card-actions .el-button {
margin-left: 10px;
}
/* 响应式调整 */
@media (max-width: 768px) {
.card-col {
width: 100%;
}
.card-item {
flex-direction: column;
}
.item-label {
margin-bottom: 4px;
}
.card-actions {
flex-direction: column;
gap: 8px;
}
.card-actions .el-button {
margin-left: 0;
width: 100%;
}
}
.el-form-item {
margin-bottom: 16px;
}
.el-card__body {
padding: 20px;
}
</style>

View File

@@ -69,7 +69,7 @@
</el-form>
</el-tab-pane>
<el-tab-pane label="订单明细" name="orderList">
<order-detail-list :orderId="currentOrder.orderId" />
<order-detail-list :orderId="currentOrder.orderId" :editable="false" />
</el-tab-pane>
<el-tab-pane label="应收明细" name="receivable">
<KLPTable v-loading="rightLoading" :data="currentOrder.receivables" empty-text="暂无数据">

View File

@@ -16,7 +16,7 @@
</el-descriptions-item>
</el-descriptions> -->
<el-row :gutter="10" class="mb8">
<el-row :gutter="10" class="mb8" v-if="editable">
<el-col :span="1.5">
<el-button type="primary" plain icon="el-icon-plus" size="mini" :disabled="!canEdit"
@click="handleAdd">新增</el-button>
@@ -155,6 +155,10 @@ export default {
orderId: {
type: [String, Number],
required: true
},
editable: {
type: Boolean,
default: true
}
},
components: {