Files
emsapp/pages/work/report/index.vue
2026-04-19 20:56:06 +08:00

581 lines
14 KiB
Vue

<template>
<view class="report-page">
<view class="toolbar">
<uni-datetime-picker v-model="dateRange" type="daterange" :end="defaultDateRange[1]" :clear-icon="false"
rangeSeparator="至" @change="changeDateRange" />
<view class="button-group">
<button size="mini" class="small" :disabled="loading" @click="resetDateRange">重置</button>
<button type="primary" size="mini" class="large" :disabled="loading" @click="getReportData">查询</button>
</view>
</view>
<view v-if="loading" class="report-state">报表数据加载中...</view>
<view v-else-if="reportList.length === 0" class="report-state">暂无报表数据</view>
<view v-else class="report-list">
<uni-collapse class="report-collapse">
<uni-collapse-item v-for="item in reportList" :key="item.dataTime" class="report-card">
<template v-slot:title>
<view class="report-card__header">
<view class="report-card__title-main">
<view class="report-card__date">{{item.dataTime}}</view>
<view class="report-card__tags">
<text v-if="item.dayType" class="report-tag">{{item.dayType}}</text>
<text v-if="item.weatherDesc" class="report-tag weather">{{item.weatherDesc}}</text>
</view>
</view>
<view class="report-summary report-summary--compact">
<view class="summary-item emphasis">
<text class="label">实际收益</text>
<text class="value">{{formatReportValue(item.actualRevenue)}}</text>
</view>
<view class="summary-item">
<text class="label">效率(%)</text>
<text class="value">{{formatReportValue(item.effect)}}</text>
</view>
<view class="summary-item">
<text class="label">充电总量(kWh)</text>
<text class="value">{{formatReportValue(item.activeTotalKwh)}}</text>
</view>
<view class="summary-item">
<text class="label">放电总量(kWh)</text>
<text class="value">{{formatReportValue(item.reActiveTotalKwh)}}</text>
</view>
</view>
</view>
</template>
<view class="report-card__content">
<view class="report-group">
<view class="report-group__title">电量数据</view>
<view class="report-metric-row">
<view class="report-metric-row__label">充电量</view>
<view class="report-metric-grid">
<view v-for="metric in buildMetricItems(item, 'chargeKwh')" :key="item.dataTime + metric.key"
class="report-metric">
<text class="metric-label">{{metric.label}}</text>
<text class="metric-value">{{formatReportValue(metric.value)}}</text>
</view>
</view>
</view>
<view class="report-metric-row">
<view class="report-metric-row__label">放电量</view>
<view class="report-metric-grid">
<view v-for="metric in buildMetricItems(item, 'dischargeKwh')"
:key="item.dataTime + metric.key" class="report-metric">
<text class="metric-label">{{metric.label}}</text>
<text class="metric-value">{{formatReportValue(metric.value)}}</text>
</view>
</view>
</view>
</view>
<view class="report-group">
<view class="report-group__title">收益数据</view>
<view class="report-metric-row">
<view class="report-metric-row__label">充电价格</view>
<view class="report-metric-grid">
<view v-for="metric in buildMetricItems(item, 'chargePrice')"
:key="item.dataTime + metric.key + 'price'" class="report-metric">
<text class="metric-label">{{metric.label}}</text>
<text class="metric-value">{{formatReportValue(metric.value)}}</text>
</view>
</view>
</view>
<view class="report-metric-row">
<view class="report-metric-row__label">放电价格</view>
<view class="report-metric-grid">
<view v-for="metric in buildMetricItems(item, 'dischargePrice')"
:key="item.dataTime + metric.key + 'rePrice'" class="report-metric">
<text class="metric-label">{{metric.label}}</text>
<text class="metric-value">{{formatReportValue(metric.value)}}</text>
</view>
</view>
</view>
</view>
</view>
</uni-collapse-item>
</uni-collapse>
</view>
</view>
</template>
<script>
import {
formatDate
} from '@/utils/filters'
import {
getAmmeterData,
getAmmeterRevenueData
} from '@/api/ems/site.js'
export default {
data() {
return {
siteId: '',
dateRange: [],
defaultDateRange: [],
reportList: [],
loading: false
}
},
methods: {
initDateRange() {
const now = new Date()
const firstDay = new Date(now.getFullYear(), now.getMonth(), 1)
this.defaultDateRange = [formatDate(firstDay), formatDate(now)]
this.dateRange = [...this.defaultDateRange]
},
changeDateRange(value) {
this.dateRange = value || []
},
resetDateRange() {
this.dateRange = [...this.defaultDateRange]
this.getReportData()
},
createEmptyReportRow(dataTime) {
return {
dataTime,
dayType: '',
weatherDesc: '',
activePeakKwh: null,
activeHighKwh: null,
activeFlatKwh: null,
activeValleyKwh: null,
activeTotalKwh: null,
reActivePeakKwh: null,
reActiveHighKwh: null,
reActiveFlatKwh: null,
reActiveValleyKwh: null,
reActiveTotalKwh: null,
effect: null,
activePeakPrice: null,
activeHighPrice: null,
activeFlatPrice: null,
activeValleyPrice: null,
activeTotalPrice: null,
reActivePeakPrice: null,
reActiveHighPrice: null,
reActiveFlatPrice: null,
reActiveValleyPrice: null,
reActiveTotalPrice: null,
actualRevenue: null
}
},
mergeReportRows(ammeterRows = [], revenueRows = []) {
const reportMap = {}
const ensureRow = (date) => {
const key = String(date || '').trim()
if (!key) return null
if (!reportMap[key]) {
reportMap[key] = this.createEmptyReportRow(key)
}
return reportMap[key]
}
;(ammeterRows || []).forEach(item => {
const row = ensureRow(item.dataTime)
if (!row) return
Object.assign(row, {
activePeakKwh: item.activePeakKwh,
activeHighKwh: item.activeHighKwh,
activeFlatKwh: item.activeFlatKwh,
activeValleyKwh: item.activeValleyKwh,
activeTotalKwh: item.activeTotalKwh,
reActivePeakKwh: item.reActivePeakKwh,
reActiveHighKwh: item.reActiveHighKwh,
reActiveFlatKwh: item.reActiveFlatKwh,
reActiveValleyKwh: item.reActiveValleyKwh,
reActiveTotalKwh: item.reActiveTotalKwh,
effect: item.effect
})
})
;(revenueRows || []).forEach(item => {
const row = ensureRow(item.dataTime)
if (!row) return
Object.assign(row, {
dayType: item.dayType,
weatherDesc: item.weatherDesc,
activePeakPrice: item.activePeakPrice,
activeHighPrice: item.activeHighPrice,
activeFlatPrice: item.activeFlatPrice,
activeValleyPrice: item.activeValleyPrice,
activeTotalPrice: item.activeTotalPrice,
reActivePeakPrice: item.reActivePeakPrice,
reActiveHighPrice: item.reActiveHighPrice,
reActiveFlatPrice: item.reActiveFlatPrice,
reActiveValleyPrice: item.reActiveValleyPrice,
reActiveTotalPrice: item.reActiveTotalPrice,
actualRevenue: item.actualRevenue
})
})
return Object.values(reportMap).sort((a, b) => String(b.dataTime).localeCompare(String(a.dataTime)))
},
buildMetricItems(item, type) {
const configMap = {
chargeKwh: [{
key: 'activePeakKwh',
label: '尖',
value: item.activePeakKwh
},
{
key: 'activeHighKwh',
label: '峰',
value: item.activeHighKwh
},
{
key: 'activeFlatKwh',
label: '平',
value: item.activeFlatKwh
},
{
key: 'activeValleyKwh',
label: '谷',
value: item.activeValleyKwh
},
{
key: 'activeTotalKwh',
label: '总',
value: item.activeTotalKwh
}
],
dischargeKwh: [{
key: 'reActivePeakKwh',
label: '尖',
value: item.reActivePeakKwh
},
{
key: 'reActiveHighKwh',
label: '峰',
value: item.reActiveHighKwh
},
{
key: 'reActiveFlatKwh',
label: '平',
value: item.reActiveFlatKwh
},
{
key: 'reActiveValleyKwh',
label: '谷',
value: item.reActiveValleyKwh
},
{
key: 'reActiveTotalKwh',
label: '总',
value: item.reActiveTotalKwh
}
],
chargePrice: [{
key: 'activePeakPrice',
label: '尖',
value: item.activePeakPrice
},
{
key: 'activeHighPrice',
label: '峰',
value: item.activeHighPrice
},
{
key: 'activeFlatPrice',
label: '平',
value: item.activeFlatPrice
},
{
key: 'activeValleyPrice',
label: '谷',
value: item.activeValleyPrice
},
{
key: 'activeTotalPrice',
label: '总',
value: item.activeTotalPrice
}
],
dischargePrice: [{
key: 'reActivePeakPrice',
label: '尖',
value: item.reActivePeakPrice
},
{
key: 'reActiveHighPrice',
label: '峰',
value: item.reActiveHighPrice
},
{
key: 'reActiveFlatPrice',
label: '平',
value: item.reActiveFlatPrice
},
{
key: 'reActiveValleyPrice',
label: '谷',
value: item.reActiveValleyPrice
},
{
key: 'reActiveTotalPrice',
label: '总',
value: item.reActiveTotalPrice
}
]
}
return configMap[type] || []
},
formatReportValue(value) {
if (!(value || value === 0 || value === '0')) return '-'
const num = Number(value)
if (!Number.isFinite(num)) return value
return num.toFixed(3).replace(/\.?0+$/, '')
},
getReportData() {
if (!this.siteId || !this.dateRange.length) return
const [startTime = '', endTime = ''] = this.dateRange || []
this.loading = true
Promise.all([
getAmmeterData({
siteId: this.siteId,
startTime,
endTime,
pageSize: 200,
pageNum: 1
}),
getAmmeterRevenueData({
siteId: this.siteId,
startTime,
endTime,
pageSize: 200,
pageNum: 1
})
]).then(([ammeterResponse, revenueResponse]) => {
const ammeterRows = ammeterResponse?.rows || []
const revenueRows = revenueResponse?.rows || []
this.reportList = this.mergeReportRows(ammeterRows, revenueRows)
}).catch(() => {
this.reportList = []
}).finally(() => {
this.loading = false
})
}
},
onLoad(options) {
this.siteId = options.siteId || ''
this.initDateRange()
this.getReportData()
}
}
</script>
<style lang="scss" scoped>
.report-page {
min-height: 100%;
padding: 24rpx;
background: #f5f6f8;
box-sizing: border-box;
}
.toolbar {
padding: 24rpx;
border-radius: 20rpx;
background: #fff;
margin-bottom: 24rpx;
}
.button-group {
margin-top: 20rpx;
uni-button {
padding-left: 0;
padding-right: 0;
text-align: center;
font-size: 26rpx;
&:not(:last-child) {
margin-right: 20rpx;
}
}
.small {
width: 120rpx;
}
.large {
width: 160rpx;
background-color: #547ef4;
&[disabled][type=primary] {
background-color: #89a8ffe6;
}
}
}
.report-state {
padding: 80rpx 0;
text-align: center;
color: #999;
font-size: 24rpx;
line-height: 36rpx;
}
.report-card {
margin-bottom: 20rpx;
border-radius: 20rpx;
overflow: hidden;
background: #fff;
}
.report-collapse {
:deep(.uni-collapse) {
border: 0;
}
:deep(.uni-collapse-item) {
margin-bottom: 20rpx;
border: 0;
border-radius: 20rpx;
overflow: hidden;
background: #fff;
}
:deep(.uni-collapse-item__title-wrap) {
padding: 0;
}
:deep(.uni-collapse-item__title-box) {
padding: 0;
}
:deep(.uni-collapse-item__content) {
padding: 0;
}
}
.report-card__header {
padding: 24rpx;
}
.report-card__title-main {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 20rpx;
margin-bottom: 24rpx;
}
.report-card__date {
font-size: 30rpx;
font-weight: 700;
color: #1f2329;
}
.report-card__tags {
display: flex;
flex-wrap: wrap;
justify-content: flex-end;
gap: 12rpx;
}
.report-tag {
padding: 8rpx 18rpx;
border-radius: 999rpx;
background: #eef3ff;
color: #547ef4;
font-size: 22rpx;
line-height: 1;
&.weather {
background: #f5f7fa;
color: #606266;
}
}
.report-summary {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 18rpx;
}
.report-summary--compact {
margin-bottom: 0;
}
.summary-item {
padding: 18rpx 20rpx;
border-radius: 16rpx;
background: #f7f8fa;
&.emphasis {
background: linear-gradient(135deg, #547ef4, #6ea1ff);
.label,
.value {
color: #fff;
}
}
.label {
display: block;
font-size: 22rpx;
color: #8a919f;
margin-bottom: 10rpx;
}
.value {
font-size: 30rpx;
font-weight: 700;
color: #1f2329;
line-height: 1.2;
word-break: break-all;
}
}
.report-group:not(:last-child) {
margin-bottom: 20rpx;
}
.report-card__content {
padding: 0 24rpx 24rpx;
}
.report-group__title {
margin-bottom: 16rpx;
font-size: 24rpx;
font-weight: 600;
color: #1f2329;
}
.report-metric-row:not(:last-child) {
margin-bottom: 16rpx;
}
.report-metric-row__label {
margin-bottom: 12rpx;
font-size: 22rpx;
color: #8a919f;
}
.report-metric-grid {
display: grid;
grid-template-columns: repeat(5, minmax(0, 1fr));
gap: 12rpx;
}
.report-metric {
padding: 14rpx 10rpx;
border-radius: 12rpx;
background: #f7f8fa;
text-align: center;
}
.metric-label {
display: block;
margin-bottom: 8rpx;
font-size: 20rpx;
color: #8a919f;
line-height: 1;
}
.metric-value {
display: block;
font-size: 22rpx;
font-weight: 600;
color: #1f2329;
line-height: 1.3;
word-break: break-all;
}
</style>