This commit is contained in:
2026-04-19 20:56:06 +08:00
parent ac7dd9dd30
commit 4caf7ff1ab
9 changed files with 727 additions and 60 deletions

View File

@ -129,6 +129,15 @@ export function getAmmeterRevenueData(params) {
})
}
// 电表报表
export function getAmmeterData(params) {
return request({
url: `/ems/statsReport/getAmmeterData`,
method: 'get',
params
})
}
// 一周冲放曲线
export function getSevenChargeData(data) {
return request({

View File

@ -1,5 +1,6 @@
<template>
<view class="site-switch-header">
<view class="selector-row">
<uni-data-picker
placeholder="请选择"
popup-title="业态选择"
@ -10,6 +11,8 @@
:ellipsis="false"
@change="handleChange"
/>
<view class="site-count-badge">{{ displaySiteCount }}</view>
</view>
<view class="info">
<view class="list">
<uni-icons type="location" color="#fff" size="20"></uni-icons>
@ -41,6 +44,19 @@
runningTime: {
type: String,
default: '-'
},
siteCount: {
type: [String, Number],
default: 0
}
},
computed: {
displaySiteCount() {
const count = Number(this.siteCount || 0)
if (!Number.isFinite(count) || count <= 0) {
return '0'
}
return count > 99 ? '99+' : String(count)
}
},
methods: {
@ -58,6 +74,30 @@
padding-bottom: 100rpx;
color: #fff;
.selector-row {
display: flex;
align-items: center;
gap: 16rpx;
}
.site-count-badge {
min-width: 48rpx;
height: 48rpx;
padding: 0 12rpx;
border-radius: 999rpx;
background: #ff4d4f;
display: flex;
align-items: center;
justify-content: center;
font-size: 24rpx;
line-height: 1;
font-weight: 700;
color: #fff;
box-sizing: border-box;
flex-shrink: 0;
box-shadow: 0 6rpx 14rpx rgba(255, 77, 79, 0.28);
}
.info {
color: #fff;
font-size: 26rpx;

View File

@ -3,9 +3,11 @@ module.exports = {
// todo 打包项目时切换baseUrl
// baseUrl: 'http://localhost:8089',
// 测试环境
baseUrl: 'http://110.40.171.179:8089',
// baseUrl: 'http://110.40.171.179:8089',
// 生产环境
//baseUrl: 'http://1.15.120.242:8089',
baseUrl: 'http://111.229.210.50:8089',
// 应用信息
appInfo: {
// 应用名称

View File

@ -120,6 +120,12 @@
"navigationBarTitleText": "单体电池",
"onReachBottomDistance": 100
}
},
{
"path": "pages/work/report/index",
"style": {
"navigationBarTitleText": "报表"
}
}
],

View File

@ -1,7 +1,7 @@
<template>
<view class="home-container">
<site-switch-header :site-id="siteId" :site-type-options="siteTypeOptions" :site-address="baseInfo.siteAddress"
:running-time="baseInfo.runningTime" @change="selectedSite" />
:running-time="baseInfo.runningTime" :site-count="siteOptions.length" @change="selectedSite" />
<view class="base-info">
<view class="map-card">

View File

@ -55,8 +55,8 @@
register: false,
globalConfig: getApp().globalData.config,
loginForm: {
username: "admin",
password: "admin123",
username: "",
password: "",
code: "",
uuid: ""
}

View File

@ -1,7 +1,7 @@
<template>
<view class="work-container">
<site-switch-header :site-id="siteId" :site-type-options="siteTypeOptions" :site-address="baseInfo.siteAddress"
:running-time="baseInfo.runningTime" @change="selectedSite" />
:running-time="baseInfo.runningTime" :site-count="siteCount" @change="selectedSite" />
<!-- 静态信息 -->
<view class="base-info">
<uni-group mode="card" class="install-data">
@ -26,7 +26,8 @@
<uni-grid :column="4" :showBorder="false" @change="toDetail">
<uni-grid-item v-for="(item,index) in siteGirdList" :index="index" :key="index+'work'">
<view class="grid-item-box work-box">
<view class="icon iconfont" :class="item.icon" size="30"></view>
<image v-if="item.image" :src="item.image" class="icon icon-image" mode="aspectFit" />
<view v-else class="icon iconfont" :class="item.icon" size="30"></view>
<text class="text">{{item.text}}</text>
</view>
</uni-grid-item>
@ -187,14 +188,26 @@
icon: 'icon-dantidianchi',
text: '单体电池',
categoryName: 'BATTERY'
},
{
page: 'report',
icon: 'icon-service',
image: '/static/images/work/report.svg',
text: '报表',
categoryName: ''
}
]
}
},
computed: {
...mapGetters(['belongSite', 'currentSiteId']),
siteCount() {
return (this.siteTypeOptions || []).reduce((count, typeItem) => {
return count + ((typeItem.children || []).length)
}, 0)
},
siteGirdList() {
return this.gridList.filter(i => this.deviceCategoryOptions.includes(i.categoryName))
return this.gridList.filter(i => !i.categoryName || this.deviceCategoryOptions.includes(i.categoryName))
}
},
methods: {
@ -571,6 +584,13 @@
color: #547ef4;
}
.icon-image {
width: 56rpx;
height: 56rpx;
display: block;
margin: 0 auto;
}
.text {
font-size: 26rpx;
padding-top: 10rpx;
@ -578,8 +598,6 @@
}
}
.base-lists {
font-size: 24rpx;
line-height: 40rpx;

580
pages/work/report/index.vue Normal file
View File

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

View File

@ -0,0 +1,12 @@
<svg width="128" height="128" viewBox="0 0 128 128" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="20" y="12" width="88" height="104" rx="16" fill="#EEF4FF"/>
<path d="M44 12H88L108 32V40H44V12Z" fill="#DCE8FF"/>
<path d="M88 12V28C88 30.2091 89.7909 32 92 32H108" fill="#C8DAFF"/>
<rect x="36" y="52" width="56" height="8" rx="4" fill="#9FB9FF"/>
<rect x="36" y="68" width="40" height="8" rx="4" fill="#C5D6FF"/>
<rect x="38" y="88" width="10" height="16" rx="5" fill="#7AA2FF"/>
<rect x="56" y="78" width="10" height="26" rx="5" fill="#5B87F7"/>
<rect x="74" y="70" width="10" height="34" rx="5" fill="#2F67E8"/>
<rect x="92" y="82" width="10" height="22" rx="5" fill="#89ACFF"/>
<rect x="20" y="12" width="88" height="104" rx="16" stroke="#5B87F7" stroke-width="4"/>
</svg>

After

Width:  |  Height:  |  Size: 807 B