l3能源成本分摊(部分完成留存)

This commit is contained in:
2025-12-07 17:23:47 +08:00
parent b6328a94da
commit 59951b77c3
100 changed files with 14350 additions and 847 deletions

View File

@@ -0,0 +1,152 @@
import request from '@/utils/request'
// ==================== 能源-库区映射 ====================
// 查询能源-库区映射列表
export function listEnergyLink(query) {
return request({
url: '/ems/energy/link/list',
method: 'get',
params: query
})
}
// 查询能源-库区映射矩阵
export function getEnergyLinkMatrix(query) {
return request({
url: '/ems/energy/link/matrix',
method: 'get',
params: query
})
}
// 查询能源-库区映射统计
export function getEnergyLinkStatistics() {
return request({
url: '/ems/energy/link/statistics',
method: 'get'
})
}
// 新增能源-库区映射
export function addEnergyLink(data) {
return request({
url: '/ems/energy/link',
method: 'post',
data: data
})
}
// 修改能源-库区映射
export function updateEnergyLink(data) {
return request({
url: '/ems/energy/link',
method: 'put',
data: data
})
}
// 删除能源-库区映射
export function deleteEnergyLink(linkIds) {
return request({
url: '/ems/energy/link/' + linkIds,
method: 'delete'
})
}
// 导出能源-库区映射
export function exportEnergyLink(query) {
return request({
url: '/ems/energy/link/export',
method: 'post',
params: query,
responseType: 'blob'
})
}
// ==================== 能源分摊任务 ====================
// 查询能源分摊任务列表
export function listEnergyTask(query) {
return request({
url: '/ems/energy/task/list',
method: 'get',
params: query
})
}
// 触发能源分摊任务
export function runEnergyTask(data) {
return request({
url: '/ems/energy/task/run',
method: 'post',
data: data
})
}
// 重新运行能源分摊任务
export function rerunEnergyTask(taskId) {
return request({
url: '/ems/energy/task/rerun/' + taskId,
method: 'post'
})
}
// 导出能源分摊任务
export function exportEnergyTask(query) {
return request({
url: '/ems/energy/task/export',
method: 'post',
params: query,
responseType: 'blob'
})
}
// ==================== 钢卷能源成本分摊 ====================
// 查询钢卷能源成本分摊列表
export function listEnergyCoilDaily(query) {
return request({
url: '/ems/energy/coilDaily/list',
method: 'get',
params: query
})
}
// 查询待操作钢卷的能源成本(基于待操作时间范围计算)
export function listPendingActionCoilCost(query) {
return request({
url: '/ems/energy/coilDaily/pendingAction',
method: 'get',
params: query
})
}
// 导出钢卷能源成本分摊
export function exportEnergyCoilDaily(query) {
return request({
url: '/ems/energy/coilDaily/export',
method: 'post',
params: query,
responseType: 'blob'
})
}
// 检查是否有能源成本数据,如果没有则触发分摊任务
export function checkAndTriggerAllocation(query) {
return request({
url: '/ems/energy/coilDaily/checkAndTrigger',
method: 'post',
data: query
})
}
// ==================== 辅助数据查询 ====================
// 查询所有逻辑库区列表来自WMS模块
export function listWarehouse() {
return request({
url: '/wms/warehouse/list',
method: 'get'
})
}

View File

@@ -0,0 +1,18 @@
import request from '@/utils/request'
export function listEnergyCoilDaily(query) {
return request({
url: '/ems/energy/coilDaily/list',
method: 'get',
params: query
})
}
export function exportEnergyCoilDaily(query) {
return request({
url: '/ems/energy/coilDaily/export',
method: 'post',
data: query,
responseType: 'blob'
})
}

View File

@@ -42,3 +42,33 @@ export function delEnergyConsumption(energyConsumptionId) {
method: 'delete'
})
}
// 删除能耗记录(别名)
export function deleteEnergyConsumption(energyConsumptionId) {
return delEnergyConsumption(energyConsumptionId)
}
// 获取设备的上次抄表记录
export function getLastReading(meterId) {
return request({
url: '/ems/energyConsumption/lastReading/' + meterId,
method: 'get'
})
}
// 获取设备本月的累计消耗量
export function getMonthlyConsumption(meterId) {
return request({
url: '/ems/energyConsumption/monthlyConsumption/' + meterId,
method: 'get'
})
}
// 获取能耗统计信息
export function getStatistics(query) {
return request({
url: '/ems/energyConsumption/statistics',
method: 'get',
params: query
})
}

View File

@@ -0,0 +1,25 @@
import request from '@/utils/request'
export function fetchEnergyOverview(query) {
return request({
url: '/ems/energy/report/overview',
method: 'get',
params: query
})
}
export function fetchEnergySummary(query) {
return request({
url: '/ems/energy/report/summary',
method: 'get',
params: query
})
}
export function fetchEnergyDetail(query) {
return request({
url: '/ems/energy/report/detail',
method: 'get',
params: query
})
}

View File

@@ -0,0 +1,71 @@
import request from '@/utils/request'
export function listEnergyLink(query) {
return request({
url: '/ems/energy/link/list',
method: 'get',
params: query
})
}
export function getEnergyLink(linkId) {
return request({
url: `/ems/energy/link/${linkId}`,
method: 'get'
})
}
export function addEnergyLink(data) {
return request({
url: '/ems/energy/link',
method: 'post',
data
})
}
export function updateEnergyLink(data) {
return request({
url: '/ems/energy/link',
method: 'put',
data
})
}
export function delEnergyLink(linkId) {
return request({
url: `/ems/energy/link/${linkId}`,
method: 'delete'
})
}
export function fetchEnergyLinkMatrix(query) {
return request({
url: '/ems/energy/link/matrix',
method: 'get',
params: query
})
}
export function batchDeleteEnergyLinks(linkIds) {
return request({
url: '/ems/energy/link/batch',
method: 'delete',
data: linkIds
})
}
export function exportEnergyLinks(query) {
return request({
url: '/ems/energy/link/export',
method: 'post',
data: query,
responseType: 'blob'
})
}
export function getStatistics() {
return request({
url: '/ems/energy/link/statistics',
method: 'get'
})
}

View File

@@ -42,3 +42,54 @@ export function delEnergyRate(energyRateId) {
method: 'delete'
})
}
// 获取费率的梯度费率列表
export function getRateTiers(energyRateId) {
return request({
url: '/ems/energyRate/' + energyRateId + '/tiers',
method: 'get'
})
}
// 获取费率的时段费率列表
export function getRateTimePeriods(energyRateId) {
return request({
url: '/ems/energyRate/' + energyRateId + '/timePeriods',
method: 'get'
})
}
// 保存梯度费率
export function saveTiers(energyRateId, tiers) {
return request({
url: '/ems/energyRate/' + energyRateId + '/tiers',
method: 'post',
data: tiers
})
}
// 保存时段费率
export function saveTimePeriods(energyRateId, timePeriods) {
return request({
url: '/ems/energyRate/' + energyRateId + '/timePeriods',
method: 'post',
data: timePeriods
})
}
// 获取梯度的峰谷时段费率(用于梯度+峰谷组合模式)
export function getTierPeriodLinks(tierId) {
return request({
url: '/ems/rateTierPeriodLink/tier/' + tierId,
method: 'get'
})
}
// 保存梯度-时段关联费率(用于梯度+峰谷组合模式)
export function saveTierPeriodLinks(tierId, tierPeriodLinks) {
return request({
url: '/ems/rateTierPeriodLink/tier/' + tierId,
method: 'post',
data: tierPeriodLinks
})
}

View File

@@ -0,0 +1,33 @@
import request from '@/utils/request'
export function listEnergyTask(query) {
return request({
url: '/ems/energy/task/list',
method: 'get',
params: query
})
}
export function runEnergyTask(data) {
return request({
url: '/ems/energy/task/run',
method: 'post',
data
})
}
export function rerunEnergyTask(taskId) {
return request({
url: `/ems/energy/task/rerun/${taskId}`,
method: 'post'
})
}
export function exportEnergyTask(query) {
return request({
url: '/ems/energy/task/export',
method: 'post',
data: query,
responseType: 'blob'
})
}

View File

@@ -42,3 +42,19 @@ export function delLocation(locationId) {
method: 'delete'
})
}
// 获取位置及其子位置下的所有设备
export function getLocationMeters(locationId) {
return request({
url: '/ems/location/' + locationId + '/meters',
method: 'get'
})
}
// 获取位置树及其设备信息(用于拓扑图展示)
export function getLocationTree() {
return request({
url: '/ems/location/tree/topology',
method: 'get'
})
}

View File

@@ -42,3 +42,26 @@ export function delMeter(meterId) {
method: 'delete'
})
}
// 下载设备导入模板
export function downloadMeterTemplate() {
return request({
url: '/mes/meter/template',
method: 'get',
responseType: 'blob'
})
}
// 导入设备
export function importMeters(file) {
const formData = new FormData()
formData.append('file', file)
return request({
url: '/mes/meter/import',
method: 'post',
data: formData,
headers: {
'Content-Type': 'multipart/form-data'
}
})
}

View File

@@ -0,0 +1,44 @@
import request from '@/utils/request'
// 查询库区列表
export function listWarehouse(query) {
return request({
url: '/wms/warehouse/list',
method: 'get',
params: query
})
}
// 查询库区详细
export function getWarehouse(warehouseId) {
return request({
url: '/wms/warehouse/' + warehouseId,
method: 'get'
})
}
// 新增库区
export function addWarehouse(data) {
return request({
url: '/wms/warehouse',
method: 'post',
data: data
})
}
// 修改库区
export function updateWarehouse(data) {
return request({
url: '/wms/warehouse',
method: 'put',
data: data
})
}
// 删除库区
export function deleteWarehouse(warehouseId) {
return request({
url: '/wms/warehouse/' + warehouseId,
method: 'delete'
})
}

View File

@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1764900160113" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="6698" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M864 64H160c-35.296 0-64 28.704-64 64v768c0 35.296 28.704 64 64 64h704c35.296 0 64-28.704 64-64V128c0-35.296-28.704-64-64-64zM256 128h512v224H256V128zM160 896V128h32v288h640V128h32l0.064 768H160z" p-id="6699"></path><path d="M328 192h64v96h-64zM488 192h64v96h-64zM648 192h64v96h-64zM605.152 480h-135.136L352 647.488l160 40.576-82.88 169.536L672 630.208l-160-24.16z" p-id="6700"></path></svg>

After

Width:  |  Height:  |  Size: 724 B

View File

@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1764900186993" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="7711" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M880 112H144a32.09 32.09 0 0 0-32 32v736a32.09 32.09 0 0 0 32 32h736a32.09 32.09 0 0 0 32-32V144a32.09 32.09 0 0 0-32-32z m-24 744H168V168h688z" fill="#808080" p-id="7712"></path><path d="M262 438h500a32.09 32.09 0 0 0 32-32V259.85A28.93 28.93 0 0 0 765.15 231h-506.3A28.93 28.93 0 0 0 230 259.85V406a32.09 32.09 0 0 0 32 32z m24-151h452v95H286zM623 588.47c0.87-0.23 1.36-0.34 1.36-0.34a2.6 2.6 0 0 0-1.36 0.34z" fill="#808080" p-id="7713"></path><path d="M652.78 588.31a28 28 0 0 0-20.14 34.09c11.86 46.15 3.84 83.46-23.87 110.9a122.14 122.14 0 0 1-18.32 14.83c-5.1-14.58-13.79-28-25.92-39.82A124.16 124.16 0 0 0 528 683.62a28 28 0 0 0-24.48 1.61c-33.77 19.3-49.66 43.39-57.13 62.1-28.18-18.16-44.3-38.84-46.19-59.92-2.13-23.78 13.93-49.27 44.07-69.92 30.57-21 54.53-58.5 68.61-84.69 14.33 12.91 24.51 31.88 31.15 47.84A259.57 259.57 0 0 1 559.69 635a28 28 0 0 0 55.41-8.1 312.75 312.75 0 0 0-18.86-66.52c-20.07-48.89-50-81.15-86.62-93.31A28 28 0 0 0 475 482.75c-7.56 17.92-33.51 68.75-62.4 88.54-47.32 32.44-72.18 76.59-68.18 121.12 2.22 24.82 13.06 48.12 32.22 69.25 18.83 20.77 45.57 39 79.47 54.25a30.53 30.53 0 0 0 12.42 2.69 26.5 26.5 0 0 0 16.91-5.88c8.14-6.66 12-16.63 9.79-26.88-0.37-3.84-1-24.19 23.31-43.44 10.44 7.72 23.66 22.14 20.9 44.53a28 28 0 0 0 35.46 30.36c4-1.13 39.63-11.92 71.31-42.28 30.56-29.29 62.42-82 40.66-166.56a28 28 0 0 0-34.09-20.14z" fill="#808080" p-id="7714"></path></svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1764900096420" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5558" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M384 714.656C384 779.456 441.312 832 512 832s128-52.544 128-117.344c0-43.2-42.656-121.408-128-234.656-85.344 113.248-128 191.456-128 234.656z" p-id="5559"></path><path d="M864 64H160c-35.296 0-64 28.704-64 64v768c0 35.296 28.704 64 64 64h704c35.296 0 64-28.704 64-64V128c0-35.296-28.704-64-64-64zM256 128h512v224H256V128zM160 896V128h32v288h640V128h32l0.064 768H160z" p-id="5560"></path><path d="M328 192h64v96h-64zM488 192h64v96h-64zM648 192h64v96h-64z" p-id="5561"></path></svg>

After

Width:  |  Height:  |  Size: 813 B

View File

@@ -0,0 +1,344 @@
<template>
<div class="energy-link-page">
<!-- 查询条件 -->
<el-card class="search-card">
<el-form :model="queryParams" label-width="120px" size="small">
<el-row :gutter="20">
<el-col :xs="24" :sm="12" :md="6">
<el-form-item label="能源类型:">
<el-select v-model="queryParams.energyTypeId" placeholder="请选择能源类型" clearable @change="handleQuery">
<el-option v-for="item in energyTypeList" :key="item.id" :label="item.name" :value="item.id"></el-option>
</el-select>
</el-form-item>
</el-col>
<el-col :xs="24" :sm="12" :md="6">
<el-form-item label="库区:">
<el-select v-model="queryParams.warehouseId" placeholder="请选择库区" clearable @change="handleQuery">
<el-option v-for="item in warehouseList" :key="item.warehouseId" :label="item.warehouseName" :value="item.warehouseId"></el-option>
</el-select>
</el-form-item>
</el-col>
<el-col :xs="24" :sm="12" :md="6">
<el-form-item label="状态:">
<el-select v-model="queryParams.isEnabled" placeholder="请选择状态" clearable @change="handleQuery">
<el-option label="启用" :value="1"></el-option>
<el-option label="禁用" :value="0"></el-option>
</el-select>
</el-form-item>
</el-col>
<el-col :xs="24" :sm="12" :md="6">
<el-button type="primary" icon="el-icon-search" size="small" @click="handleQuery">查询</el-button>
<el-button icon="el-icon-refresh" size="small" @click="resetQuery">重置</el-button>
<el-button type="success" icon="el-icon-plus" size="small" @click="openAddDialog">新增</el-button>
</el-col>
</el-row>
</el-form>
</el-card>
<!-- 统计信息 -->
<el-row :gutter="20" class="statistics-row">
<el-col :xs="24" :sm="12" :md="6">
<div class="stat-card">
<div class="stat-label">总映射数</div>
<div class="stat-value">{{ statistics.totalLinks }}</div>
</div>
</el-col>
<el-col :xs="24" :sm="12" :md="6">
<div class="stat-card">
<div class="stat-label">库区数</div>
<div class="stat-value">{{ statistics.totalWarehouses }}</div>
</div>
</el-col>
<el-col :xs="24" :sm="12" :md="6">
<div class="stat-card">
<div class="stat-label">仪表数</div>
<div class="stat-value">{{ statistics.totalMeters }}</div>
</div>
</el-col>
<el-col :xs="24" :sm="12" :md="6">
<div class="stat-card">
<div class="stat-label">能源类型数</div>
<div class="stat-value">{{ statistics.totalEnergyTypes }}</div>
</div>
</el-col>
</el-row>
<!-- 映射列表 -->
<el-card class="list-card" v-loading="loading">
<div slot="header" class="clearfix">
<span class="card-title">能源-库区映射列表</span>
</div>
<el-table :data="linkList" stripe border>
<el-table-column prop="linkId" label="映射ID" width="100"></el-table-column>
<el-table-column prop="energyTypeName" label="能源类型" width="100"></el-table-column>
<el-table-column prop="meterCode" label="仪表编号" width="150"></el-table-column>
<el-table-column prop="warehouseName" label="逻辑库区" width="150"></el-table-column>
<el-table-column prop="allocationMode" label="分摊方式" width="120">
<template slot-scope="scope">
<el-tag v-if="scope.row.allocationMode === 'count'" type="success">按数量</el-tag>
<el-tag v-else-if="scope.row.allocationMode === 'weight'" type="warning">按重量</el-tag>
<el-tag v-else type="info">按重量×时间</el-tag>
</template>
</el-table-column>
<el-table-column prop="weightRatio" label="权重比例" width="100">
<template slot-scope="scope">
{{ formatNumber(scope.row.weightRatio, 4) }}
</template>
</el-table-column>
<el-table-column prop="isEnabled" label="状态" width="80">
<template slot-scope="scope">
<el-tag v-if="scope.row.isEnabled === 1" type="success">启用</el-tag>
<el-tag v-else type="danger">禁用</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="150" fixed="right">
<template slot-scope="scope">
<el-button type="primary" size="mini" @click="openEditDialog(scope.row)">编辑</el-button>
<el-button type="danger" size="mini" @click="deleteLink(scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<el-pagination
:current-page="queryParams.pageNum"
:page-sizes="[10, 20, 50, 100]"
:page-size="queryParams.pageSize"
layout="total, sizes, prev, pager, next, jumper"
:total="total"
@size-change="handlePageSizeChange"
@current-change="handlePageChange"
style="margin-top: 20px; text-align: right;"
></el-pagination>
</el-card>
<!-- 编辑对话框 -->
<el-dialog :title="editDialogTitle" :visible.sync="editDialogVisible" width="600px" @close="closeEditDialog">
<el-form :model="editForm" label-width="120px" size="small" ref="editFormRef">
<el-form-item label="能源类型:" prop="energyTypeId">
<el-select v-model="editForm.energyTypeId" placeholder="请选择能源类型">
<el-option v-for="item in energyTypeList" :key="item.id" :label="item.name" :value="item.id"></el-option>
</el-select>
</el-form-item>
<el-form-item label="库区:" prop="warehouseId">
<el-select v-model="editForm.warehouseId" placeholder="请选择库区">
<el-option v-for="item in warehouseList" :key="item.warehouseId" :label="item.warehouseName" :value="item.warehouseId"></el-option>
</el-select>
</el-form-item>
<el-form-item label="仪表:" prop="meterId">
<el-select v-model="editForm.meterId" placeholder="请选择仪表">
<el-option v-for="item in meterList" :key="item.id" :label="item.meterCode" :value="item.id"></el-option>
</el-select>
</el-form-item>
<el-form-item label="分摊方式:" prop="allocationMode">
<el-select v-model="editForm.allocationMode" placeholder="请选择分摊方式">
<el-option label="按数量" value="count"></el-option>
<el-option label="按重量" value="weight"></el-option>
<el-option label="按重量×时间" value="weight_time"></el-option>
</el-select>
</el-form-item>
<el-form-item label="权重比例:" prop="weightRatio">
<el-input-number v-model="editForm.weightRatio" :min="0" :max="1" :step="0.01" :precision="4"></el-input-number>
</el-form-item>
<el-form-item label="状态:" prop="isEnabled">
<el-radio-group v-model="editForm.isEnabled">
<el-radio :label="1">启用</el-radio>
<el-radio :label="0">禁用</el-radio>
</el-radio-group>
</el-form-item>
</el-form>
<span slot="footer" class="dialog-footer">
<el-button @click="closeEditDialog">取消</el-button>
<el-button type="primary" @click="saveLink">保存</el-button>
</span>
</el-dialog>
</div>
</template>
<script>
import { listEnergyLink } from '@/api/ems/energyAllocation'
export default {
name: 'EnergyLink',
data() {
return {
loading: false,
total: 0,
queryParams: {
energyTypeId: null,
warehouseId: null,
isEnabled: null,
pageNum: 1,
pageSize: 20
},
linkList: [],
energyTypeList: [
{ id: 1, name: '电' },
{ id: 2, name: '水' },
{ id: 3, name: '气' }
],
warehouseList: [],
meterList: [],
statistics: {
totalLinks: 0,
totalWarehouses: 0,
totalMeters: 0,
totalEnergyTypes: 0
},
editDialogVisible: false,
editDialogTitle: '新增映射',
editForm: {
linkId: null,
energyTypeId: null,
warehouseId: null,
meterId: null,
allocationMode: 'weight_time',
weightRatio: 1,
isEnabled: 1
}
};
},
mounted() {
this.loadWarehouseList();
this.loadMeterList();
this.handleQuery();
},
methods: {
handleQuery() {
this.loading = true;
listEnergyLink(this.queryParams).then(response => {
this.linkList = response.rows || [];
this.total = response.total || 0;
}).catch(() => {
this.$message.error('加载映射列表失败');
}).finally(() => {
this.loading = false;
});
},
resetQuery() {
this.queryParams = {
energyTypeId: null,
warehouseId: null,
isEnabled: null,
pageNum: 1,
pageSize: 20
};
this.handleQuery();
},
handlePageChange(page) {
this.queryParams.pageNum = page;
this.handleQuery();
},
handlePageSizeChange(size) {
this.queryParams.pageSize = size;
this.handleQuery();
},
loadWarehouseList() {
// TODO: 从后端加载库区列表
this.warehouseList = [
{ id: 1, name: '库区A' },
{ id: 2, name: '库区B' },
{ id: 3, name: '库区C' }
];
},
loadMeterList() {
// TODO: 从后端加载仪表列表
this.meterList = [
{ id: 1, meterCode: 'METER-001' },
{ id: 2, meterCode: 'METER-002' },
{ id: 3, meterCode: 'METER-003' }
];
},
openAddDialog() {
this.editDialogTitle = '新增映射';
this.editForm = {
linkId: null,
energyTypeId: null,
warehouseId: null,
meterId: null,
allocationMode: 'weight_time',
weightRatio: 1,
isEnabled: 1
};
this.editDialogVisible = true;
},
openEditDialog(row) {
this.editDialogTitle = '编辑映射';
this.editForm = { ...row };
this.editDialogVisible = true;
},
closeEditDialog() {
this.editDialogVisible = false;
this.$refs.editFormRef && this.$refs.editFormRef.clearValidate();
},
saveLink() {
// TODO: 调用后端API保存映射
this.$message.success('保存成功');
this.closeEditDialog();
this.handleQuery();
},
deleteLink(row) {
this.$confirm('确定删除该映射吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
// TODO: 调用后端API删除映射
this.$message.success('删除成功');
this.handleQuery();
}).catch(() => {});
},
formatNumber(value, decimals = 2) {
if (value === null || value === undefined) return '0.00';
return parseFloat(value).toFixed(decimals);
}
}
};
</script>
<style scoped>
.energy-link-page {
padding: 20px;
}
.search-card {
margin-bottom: 20px;
}
.statistics-row {
margin-bottom: 20px;
}
.stat-card {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 20px;
border-radius: 4px;
text-align: center;
}
.stat-card .stat-label {
font-size: 12px;
opacity: 0.8;
margin-bottom: 10px;
}
.stat-card .stat-value {
font-size: 28px;
font-weight: bold;
}
.list-card {
margin-bottom: 20px;
}
.card-title {
font-size: 16px;
font-weight: bold;
}
.dialog-footer {
text-align: right;
}
</style>

View File

@@ -0,0 +1,630 @@
<template>
<div class="coil-cost-list">
<!-- 统计信息 -->
<div class="statistics-section">
<div class="stat-item">
<div class="stat-label">钢卷总数</div>
<div class="stat-value">{{ statistics.totalCoils }}</div>
</div>
<div class="stat-item">
<div class="stat-label">总消耗量</div>
<div class="stat-value">{{ formatNumber(statistics.totalConsumption, 2) }}</div>
</div>
<div class="stat-item">
<div class="stat-label">总成本</div>
<div class="stat-value cost-highlight">¥ {{ formatNumber(statistics.totalCost, 2) }}</div>
</div>
<div class="stat-item">
<div class="stat-label">平均单位成本</div>
<div class="stat-value">¥ {{ formatNumber(statistics.avgUnitCost, 2) }}</div>
</div>
</div>
<!-- 检索条件 -->
<div class="search-section">
<el-row :gutter="20">
<el-col :xs="24" :sm="12" :md="6">
<el-input
v-model="queryParams.enterCoilNo"
placeholder="入场钢卷号"
clearable
@keyup.enter="handleSearch"
></el-input>
</el-col>
<el-col :xs="24" :sm="12" :md="6">
<el-input
v-model="queryParams.currentCoilNo"
placeholder="当前钢卷号"
clearable
@keyup.enter="handleSearch"
></el-input>
</el-col>
<el-col :xs="24" :sm="12" :md="6">
<el-select
v-model="queryParams.warehouseId"
placeholder="选择逻辑库区"
clearable
>
<el-option
v-for="warehouse in warehouseList"
:key="warehouse.warehouseId"
:label="warehouse.warehouseName"
:value="warehouse.warehouseId"
></el-option>
</el-select>
</el-col>
<el-col :xs="24" :sm="12" :md="6">
<el-button type="primary" @click="handleSearch" icon="el-icon-search">查询</el-button>
<el-button @click="handleReset" icon="el-icon-refresh">重置</el-button>
</el-col>
</el-row>
</div>
<!-- 钢卷成本列表 -->
<el-table :data="coilCostList" stripe border v-loading="loading" :default-sort="{ prop: 'costAmount', order: 'descending' }">
<el-table-column prop="currentCoilNo" label="当前卷号" min-width="120"></el-table-column>
<el-table-column prop="enterCoilNo" label="入场卷号" min-width="120"></el-table-column>
<el-table-column prop="warehouseName" label="逻辑库区" min-width="120"></el-table-column>
<el-table-column label="生产时间" min-width="200">
<template slot-scope="scope">
{{ formatTime(null, null, scope.row.scanTime) }} - {{ formatTime(null, null, scope.row.completeTime) }}
</template>
</el-table-column>
<el-table-column prop="productionDuration" label="生产时长(分钟)" min-width="120" align="right">
<template slot-scope="scope">
{{ formatNumber(scope.row.productionDuration / 60, 2) }}
</template>
</el-table-column>
<el-table-column prop="consumptionQty" label="综合消耗(单位)" min-width="120" align="right">
<template slot-scope="scope">
{{ formatNumber(scope.row.consumptionQty, 2) }}
</template>
</el-table-column>
<el-table-column prop="costAmount" label="综合成本(¥)" min-width="120" align="right" sortable>
<template slot-scope="scope">
<span class="cost-value">{{ formatNumber(scope.row.costAmount, 2) }}</span>
</template>
</el-table-column>
<el-table-column label="单位成本(¥/h)" min-width="120" align="right">
<template slot-scope="scope">
{{ formatNumber(scope.row.costAmount / (scope.row.productionDuration / 60), 2) }}
</template>
</el-table-column>
<el-table-column label="操作" width="100" fixed="right">
<template slot-scope="scope">
<el-button type="primary" size="small" @click="showDetail(scope.row)">详情</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<el-pagination
class="pagination"
@size-change="handlePageSizeChange"
@current-change="handlePageChange"
:current-page="queryParams.pageNum"
:page-sizes="[50, 100, 200, 500]"
:page-size="queryParams.pageSize"
layout="total, sizes, prev, pager, next, jumper"
:total="total"
></el-pagination>
<!-- 详情对话框 -->
<el-dialog title="钢卷成本详情" :visible.sync="detailDialogVisible" width="90%" @close="closeDetail">
<div v-if="selectedCoil" class="detail-content">
<!-- 基本信息 -->
<div class="section">
<h4 class="section-title">基本信息</h4>
<el-row :gutter="20">
<el-col :xs="24" :sm="12" :md="6">
<div class="info-item">
<span class="label">当前卷号:</span>
<span class="value">{{ selectedCoil.currentCoilNo }}</span>
</div>
</el-col>
<el-col :xs="24" :sm="12" :md="6">
<div class="info-item">
<span class="label">入场卷号:</span>
<span class="value">{{ selectedCoil.enterCoilNo }}</span>
</div>
</el-col>
<el-col :xs="24" :sm="12" :md="6">
<div class="info-item">
<span class="label">逻辑库区:</span>
<span class="value">{{ selectedCoil.warehouseName }}</span>
</div>
</el-col>
<el-col :xs="24" :sm="12" :md="6">
<div class="info-item">
<span class="label">生产时长:</span>
<span class="value">{{ formatNumber(selectedCoil.productionDuration / 60, 2) }} 小时</span>
</div>
</el-col>
<el-col :xs="24" :sm="12" :md="6">
<div class="info-item">
<span class="label">开始时间:</span>
<span class="value">{{ formatTime(null, null, selectedCoil.scanTime) }}</span>
</div>
</el-col>
<el-col :xs="24" :sm="12" :md="6">
<div class="info-item">
<span class="label">结束时间:</span>
<span class="value">{{ formatTime(null, null, selectedCoil.completeTime) }}</span>
</div>
</el-col>
<el-col :xs="24" :sm="12" :md="6">
<div class="info-item">
<span class="label">综合消耗:</span>
<span class="value">{{ formatNumber(selectedCoil.consumptionQty, 2) }}</span>
</div>
</el-col>
<el-col :xs="24" :sm="12" :md="6">
<div class="info-item">
<span class="label">综合成本:</span>
<span class="value highlight">¥ {{ formatNumber(selectedCoil.costAmount, 2) }}</span>
</div>
</el-col>
</el-row>
</div>
<!-- 成本汇总 -->
<div class="section">
<h4 class="section-title">成本汇总</h4>
<el-row :gutter="20">
<el-col :xs="24" :sm="12" :md="8">
<div class="info-item">
<span class="label">综合消耗量:</span>
<span class="value">{{ formatNumber(selectedCoil.consumptionQty, 2) }}</span>
</div>
</el-col>
<el-col :xs="24" :sm="12" :md="8">
<div class="info-item">
<span class="label">能源费率:</span>
<span class="value">{{ selectedCoil.energyRate ? ('¥ ' + formatNumber(selectedCoil.energyRate, 4)) : '-' }}</span>
</div>
</el-col>
<el-col :xs="24" :sm="12" :md="8">
<div class="info-item">
<span class="label">综合成本:</span>
<span class="value highlight">¥ {{ formatNumber(selectedCoil.costAmount, 2) }}</span>
</div>
</el-col>
</el-row>
<div style="margin-top: 12px; padding: 12px; background-color: #f5f7fa; border-radius: 4px;">
<p style="margin: 0; font-size: 12px; color: #606266;">
综合成本为该钢卷在生产时间内所有设备产生的能源成本总和包括电气等所有能源类型
</p>
</div>
</div>
<!-- 相关设备 -->
<div class="section" v-if="selectedCoil.relatedMeters && selectedCoil.relatedMeters.length > 0">
<h4 class="section-title">相关设备</h4>
<el-table :data="selectedCoil.relatedMeters" stripe border size="small">
<el-table-column prop="meterCode" label="设备编号" min-width="120"></el-table-column>
<el-table-column prop="energyTypeName" label="能源类型" min-width="100"></el-table-column>
<el-table-column prop="model" label="型号" min-width="100"></el-table-column>
<el-table-column prop="manufacturer" label="制造商" min-width="100"></el-table-column>
<el-table-column prop="status" label="状态" min-width="80">
<template slot-scope="scope">
<el-tag :type="scope.row.status === 0 ? 'success' : 'info'">
{{ scope.row.status === 0 ? '在用' : '停用' }}
</el-tag>
</template>
</el-table-column>
</el-table>
</div>
</div>
</el-dialog>
</div>
</template>
<script>
import { listEnergyCoilDaily, listPendingActionCoilCost } from '@/api/ems/energyAllocation'
export default {
name: 'CoilCost',
data() {
return {
loading: false,
detailDialogVisible: false,
selectedCoil: null,
coilCostList: [],
total: 0,
warehouseList: [],
statistics: {
totalCoils: 0,
totalConsumption: 0,
totalCost: 0,
avgUnitCost: 0
},
queryParams: {
pageNum: 1,
pageSize: 50,
enterCoilNo: '',
currentCoilNo: '',
warehouseId: null
},
allCoilCostList: [], // 保存所有数据用于统计
dataSource: 'pending' // 'pending' 或 'daily'
};
},
mounted() {
this.loadWarehouseList();
this.loadCoilCostList();
},
methods: {
loadWarehouseList() {
// TODO: 从后端加载库区列表,这里暂时使用空数组
// 实际应该调用 API 获取库区列表
this.warehouseList = [];
},
loadCoilCostList() {
this.loading = true;
// 优先加载待操作钢卷的成本
this.fetchPendingActionCoilCost();
},
fetchPendingActionCoilCost() {
// 先尝试加载待操作钢卷的成本
listPendingActionCoilCost({ pageNum: 1, pageSize: 10000 }).then(response => {
this.allCoilCostList = response.rows || [];
this.dataSource = 'pending';
this.applyFiltersAndPagination();
}).catch(() => {
// 如果待操作接口失败,则加载日常分摊数据
this.fetchDailyCoilCostData();
}).finally(() => {
this.loading = false;
});
},
fetchDailyCoilCostData() {
this.loading = true;
listEnergyCoilDaily({ pageNum: 1, pageSize: 10000 }).then(response => {
this.allCoilCostList = response.rows || [];
this.dataSource = 'daily';
this.applyFiltersAndPagination();
}).catch(() => {
this.$message.error('加载钢卷能源成本失败');
this.allCoilCostList = [];
}).finally(() => {
this.loading = false;
});
},
applyFiltersAndPagination() {
// 应用过滤条件
let filteredList = this.allCoilCostList;
if (this.queryParams.enterCoilNo) {
filteredList = filteredList.filter(item =>
item.enterCoilNo && item.enterCoilNo.includes(this.queryParams.enterCoilNo)
);
}
if (this.queryParams.currentCoilNo) {
filteredList = filteredList.filter(item =>
item.currentCoilNo && item.currentCoilNo.includes(this.queryParams.currentCoilNo)
);
}
if (this.queryParams.warehouseId) {
filteredList = filteredList.filter(item =>
item.warehouseId === this.queryParams.warehouseId
);
}
// 计算统计数据
this.calculateStatistics(filteredList);
// 分页处理
this.total = filteredList.length;
const start = (this.queryParams.pageNum - 1) * this.queryParams.pageSize;
const end = start + this.queryParams.pageSize;
this.coilCostList = filteredList.slice(start, end);
},
calculateStatistics(list) {
this.statistics.totalCoils = list.length;
this.statistics.totalConsumption = list.reduce((sum, item) => sum + (item.consumptionQty || 0), 0);
this.statistics.totalCost = list.reduce((sum, item) => sum + (item.costAmount || 0), 0);
// 计算平均单位成本(总成本 / 总时长小时数)
const totalHours = list.reduce((sum, item) => sum + (item.productionDuration || 0) / 60, 0);
this.statistics.avgUnitCost = totalHours > 0 ? this.statistics.totalCost / totalHours : 0;
},
handleSearch() {
this.queryParams.pageNum = 1;
this.applyFiltersAndPagination();
},
handleReset() {
this.queryParams.enterCoilNo = '';
this.queryParams.currentCoilNo = '';
this.queryParams.warehouseId = null;
this.queryParams.pageNum = 1;
this.applyFiltersAndPagination();
},
handlePageChange(pageNum) {
this.queryParams.pageNum = pageNum;
this.applyFiltersAndPagination();
},
handlePageSizeChange(pageSize) {
this.queryParams.pageSize = pageSize;
this.queryParams.pageNum = 1;
this.applyFiltersAndPagination();
},
loadMockData() {
setTimeout(() => {
this.coilCostList = [
{
coilCode: 'COIL-20231201-001',
logicWarehouseName: '库区A',
startTime: '2023-12-01 08:00:00',
endTime: '2023-12-01 16:30:00',
duration: 8.5,
totalCost: 1250.50,
energyCosts: [
{
energyTypeId: 1,
energyTypeName: '电',
consumption: 850,
consumptionUnit: 'kWh',
rate: 1.2,
cost: 1020,
percentage: 81.55
},
{
energyTypeId: 2,
energyTypeName: '水',
consumption: 120,
consumptionUnit: '吨',
rate: 1.5,
cost: 180,
percentage: 14.39
},
{
energyTypeId: 3,
energyTypeName: '气',
consumption: 45,
consumptionUnit: '立方米',
rate: 3.2,
cost: 50.5,
percentage: 4.04
}
],
relatedMeters: [
{
meterCode: 'METER-001',
energyTypeName: '电',
model: 'DL-2000',
manufacturer: '施耐德',
status: 0
},
{
meterCode: 'METER-002',
energyTypeName: '水',
model: 'WM-500',
manufacturer: '西门子',
status: 0
}
]
},
{
coilCode: 'COIL-20231201-002',
logicWarehouseName: '库区B',
startTime: '2023-12-01 09:00:00',
endTime: '2023-12-01 17:00:00',
duration: 8.0,
totalCost: 980.30,
energyCosts: [
{
energyTypeId: 1,
energyTypeName: '电',
consumption: 700,
consumptionUnit: 'kWh',
rate: 1.2,
cost: 840,
percentage: 85.75
},
{
energyTypeId: 2,
energyTypeName: '水',
consumption: 80,
consumptionUnit: '吨',
rate: 1.5,
cost: 120,
percentage: 12.24
},
{
energyTypeId: 3,
energyTypeName: '气',
consumption: 20,
consumptionUnit: '立方米',
rate: 3.2,
cost: 20.3,
percentage: 2.07
}
],
relatedMeters: [
{
meterCode: 'METER-003',
energyTypeName: '电',
model: 'DL-2000',
manufacturer: '施耐德',
status: 0
}
]
},
{
coilCode: 'COIL-20231201-003',
logicWarehouseName: '库区C',
startTime: '2023-12-01 10:00:00',
endTime: '2023-12-01 18:30:00',
duration: 8.5,
totalCost: 1520.80,
energyCosts: [
{
energyTypeId: 1,
energyTypeName: '电',
consumption: 950,
consumptionUnit: 'kWh',
rate: 1.2,
cost: 1140,
percentage: 74.93
},
{
energyTypeId: 2,
energyTypeName: '水',
consumption: 150,
consumptionUnit: '吨',
rate: 1.5,
cost: 225,
percentage: 14.79
},
{
energyTypeId: 3,
energyTypeName: '气',
consumption: 80,
consumptionUnit: '立方米',
rate: 3.2,
cost: 155.8,
percentage: 10.24
}
],
relatedMeters: [
{
meterCode: 'METER-004',
energyTypeName: '电',
model: 'DL-3000',
manufacturer: '施耐德',
status: 0
},
{
meterCode: 'METER-005',
energyTypeName: '水',
model: 'WM-600',
manufacturer: '西门子',
status: 0
}
]
}
];
}, 500);
},
showDetail(coil) {
this.selectedCoil = coil;
this.detailDialogVisible = true;
},
closeDetail() {
this.selectedCoil = null;
this.detailDialogVisible = false;
},
formatTime(row, column, cellValue) {
if (!cellValue) return '-';
return new Date(cellValue).toLocaleString('zh-CN');
},
formatNumber(value, decimals = 2) {
if (value === null || value === undefined) return '0.00';
return parseFloat(value).toFixed(decimals);
}
}
};
</script>
<style scoped lang="scss">
.coil-cost-list {
padding: 20px;
background: #f6f7fb;
min-height: calc(100vh - 100px);
.statistics-section {
display: flex;
gap: 20px;
margin-bottom: 20px;
padding: 20px;
background: #fafafa;
border-radius: 4px;
border: 1px solid #ebeef5;
.stat-item {
flex: 1;
text-align: center;
.stat-label {
font-size: 14px;
color: #606266;
margin-bottom: 10px;
}
.stat-value {
font-size: 32px;
font-weight: bold;
color: #409eff;
&.cost-highlight {
color: #f56c6c;
}
}
}
}
.search-section {
background: #ffffff;
padding: 16px;
border-radius: 10px;
margin-bottom: 20px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
::v-deep .el-input,
::v-deep .el-select {
width: 100%;
}
}
.pagination {
margin-top: 20px;
text-align: right;
}
.cost-value {
color: #f56c6c;
font-weight: bold;
}
.detail-content {
.section {
margin-bottom: 24px;
.section-title {
font-size: 14px;
font-weight: bold;
color: #303133;
margin-bottom: 12px;
padding-bottom: 8px;
border-bottom: 2px solid #409eff;
}
}
.info-item {
display: flex;
flex-direction: column;
gap: 4px;
.label {
font-size: 12px;
color: #909399;
font-weight: 500;
}
.value {
font-size: 14px;
color: #303133;
font-weight: 500;
&.highlight {
color: #f56c6c;
font-size: 16px;
font-weight: bold;
}
}
}
}
}
</style>

View File

@@ -0,0 +1,572 @@
<template>
<div class="energy-cost-summary-page">
<!-- 查询条件 -->
<el-card class="search-card">
<el-form :model="queryParams" label-width="100px" size="small">
<el-row :gutter="20">
<el-col :xs="24" :sm="12" :md="6">
<el-form-item label="统计维度:">
<el-select v-model="queryParams.dimension" placeholder="请选择维度" @change="handleQuery">
<el-option label="按钢卷" value="coil"></el-option>
<el-option label="按库区" value="warehouse"></el-option>
<el-option label="按时间" value="time"></el-option>
<el-option label="按能源" value="energy"></el-option>
</el-select>
</el-form-item>
</el-col>
<el-col :xs="24" :sm="12" :md="6">
<el-form-item label="开始日期:">
<el-date-picker v-model="queryParams.startDate" type="date" placeholder="选择开始日期" value-format="yyyy-MM-dd" @change="handleQuery"></el-date-picker>
</el-form-item>
</el-col>
<el-col :xs="24" :sm="12" :md="6">
<el-form-item label="结束日期:">
<el-date-picker v-model="queryParams.endDate" type="date" placeholder="选择结束日期" value-format="yyyy-MM-dd" @change="handleQuery"></el-date-picker>
</el-form-item>
</el-col>
<el-col :xs="24" :sm="12" :md="6">
<el-button type="primary" icon="el-icon-search" size="small" @click="handleQuery">查询</el-button>
<el-button icon="el-icon-refresh" size="small" @click="resetQuery">重置</el-button>
</el-col>
</el-row>
</el-form>
</el-card>
<!-- 按钢卷统计 -->
<div v-if="queryParams.dimension === 'coil'">
<el-card class="summary-card" v-loading="loading">
<div slot="header" class="clearfix">
<span class="card-title">钢卷能源成本汇总</span>
</div>
<el-table :data="coilSummaryList" stripe border max-height="600">
<el-table-column prop="coilCode" label="钢卷编号" width="150"></el-table-column>
<el-table-column prop="logicWarehouseName" label="逻辑库区" width="120"></el-table-column>
<el-table-column prop="startTime" label="开始时间" width="180">
<template slot-scope="scope">
{{ formatTime(scope.row.startTime) }}
</template>
</el-table-column>
<el-table-column prop="endTime" label="完成时间" width="180">
<template slot-scope="scope">
{{ formatTime(scope.row.endTime) }}
</template>
</el-table-column>
<el-table-column prop="duration" label="生产时长" width="100">
<template slot-scope="scope">
{{ formatNumber(scope.row.duration, 1) }}h
</template>
</el-table-column>
<el-table-column prop="totalCost" label="总成本" width="120">
<template slot-scope="scope">
<span class="cost-value">¥ {{ formatNumber(scope.row.totalCost, 2) }}</span>
</template>
</el-table-column>
<el-table-column prop="unitCost" label="单位成本" width="120">
<template slot-scope="scope">
¥ {{ formatNumber(scope.row.unitCost, 2) }}/h
</template>
</el-table-column>
<el-table-column label="操作" width="100" fixed="right">
<template slot-scope="scope">
<el-button type="primary" size="mini" @click="viewCoilDetail(scope.row)">详情</el-button>
</template>
</el-table-column>
</el-table>
</el-card>
</div>
<!-- 按库区统计 -->
<div v-if="queryParams.dimension === 'warehouse'">
<el-card class="summary-card" v-loading="loading">
<div slot="header" class="clearfix">
<span class="card-title">库区能源成本汇总</span>
</div>
<el-table :data="warehouseSummaryList" stripe border max-height="600">
<el-table-column prop="logicWarehouseName" label="逻辑库区" width="150"></el-table-column>
<el-table-column prop="coilCount" label="生产钢卷数" width="120">
<template slot-scope="scope">
{{ scope.row.coilCount }}
</template>
</el-table-column>
<el-table-column prop="totalDuration" label="总生产时长" width="120">
<template slot-scope="scope">
{{ formatNumber(scope.row.totalDuration, 1) }}h
</template>
</el-table-column>
<el-table-column prop="totalCost" label="总成本" width="120">
<template slot-scope="scope">
<span class="cost-value">¥ {{ formatNumber(scope.row.totalCost, 2) }}</span>
</template>
</el-table-column>
<el-table-column prop="averageUnitCost" label="平均单位成本" width="140">
<template slot-scope="scope">
¥ {{ formatNumber(scope.row.averageUnitCost, 2) }}/
</template>
</el-table-column>
<el-table-column label="操作" width="100" fixed="right">
<template slot-scope="scope">
<el-button type="primary" size="mini" @click="viewWarehouseDetail(scope.row)">详情</el-button>
</template>
</el-table-column>
</el-table>
</el-card>
</div>
<!-- 按时间统计 -->
<div v-if="queryParams.dimension === 'time'">
<el-card class="chart-card" v-loading="loading">
<div slot="header" class="clearfix">
<span class="card-title">能源成本趋势</span>
</div>
<div id="costTrendChart" style="height: 400px;"></div>
</el-card>
<el-card class="summary-card" v-loading="loading">
<div slot="header" class="clearfix">
<span class="card-title">按日期统计</span>
</div>
<el-table :data="timeSummaryList" stripe border max-height="600">
<el-table-column prop="date" label="日期" width="150"></el-table-column>
<el-table-column prop="coilCount" label="生产钢卷数" width="120">
<template slot-scope="scope">
{{ scope.row.coilCount }}
</template>
</el-table-column>
<el-table-column prop="totalDuration" label="总生产时长" width="120">
<template slot-scope="scope">
{{ formatNumber(scope.row.totalDuration, 1) }}h
</template>
</el-table-column>
<el-table-column prop="totalCost" label="总成本" width="120">
<template slot-scope="scope">
<span class="cost-value">¥ {{ formatNumber(scope.row.totalCost, 2) }}</span>
</template>
</el-table-column>
<el-table-column prop="averageUnitCost" label="平均单位成本" width="140">
<template slot-scope="scope">
¥ {{ formatNumber(scope.row.averageUnitCost, 2) }}/
</template>
</el-table-column>
</el-table>
</el-card>
</div>
<!-- 按能源统计 -->
<div v-if="queryParams.dimension === 'energy'">
<el-card class="chart-card" v-loading="loading">
<div slot="header" class="clearfix">
<span class="card-title">能源成本占比</span>
</div>
<div id="energyCostPieChart" style="height: 400px;"></div>
</el-card>
<el-card class="summary-card" v-loading="loading">
<div slot="header" class="clearfix">
<span class="card-title">能源成本明细</span>
</div>
<el-table :data="energySummaryList" stripe border max-height="600">
<el-table-column prop="energyTypeName" label="能源类型" width="150"></el-table-column>
<el-table-column prop="totalConsumption" label="总消耗量" width="120">
<template slot-scope="scope">
{{ formatNumber(scope.row.totalConsumption, 2) }}
</template>
</el-table-column>
<el-table-column prop="consumptionUnit" label="单位" width="100"></el-table-column>
<el-table-column prop="totalCost" label="总成本" width="120">
<template slot-scope="scope">
<span class="cost-value">¥ {{ formatNumber(scope.row.totalCost, 2) }}</span>
</template>
</el-table-column>
<el-table-column prop="percentage" label="成本占比" width="120">
<template slot-scope="scope">
{{ formatNumber(scope.row.percentage, 2) }}%
</template>
</el-table-column>
<el-table-column prop="averageRate" label="平均费率" width="120">
<template slot-scope="scope">
¥ {{ formatNumber(scope.row.averageRate, 4) }}
</template>
</el-table-column>
</el-table>
</el-card>
</div>
</div>
</template>
<script>
import * as echarts from 'echarts'
import { listEnergyCoilDaily } from '@/api/ems/energyAllocation'
export default {
name: 'EnergyCostSummary',
data() {
return {
loading: false,
queryParams: {
dimension: 'coil',
startDate: '',
endDate: '',
pageNum: 1,
pageSize: 100
},
coilSummaryList: [],
warehouseSummaryList: [],
timeSummaryList: [],
energySummaryList: [],
chartInstances: {}
};
},
mounted() {
this.handleQuery();
},
methods: {
handleQuery() {
this.loading = true;
// 调用后端API获取钢卷能源成本数据
listEnergyCoilDaily(this.queryParams).then(response => {
const data = response.rows || [];
// 根据选择的维度处理数据
if (this.queryParams.dimension === 'coil') {
this.coilSummaryList = data;
} else {
this.processDataByDimension(data);
}
this.initCharts();
}).catch(() => {
this.$message.error('加载能源成本数据失败');
// 加载模拟数据作为备用
this.loadMockData();
}).finally(() => {
this.loading = false;
});
},
processDataByDimension(data) {
if (this.queryParams.dimension === 'warehouse') {
// 按库区聚合
const warehouseMap = {};
data.forEach(item => {
const key = item.warehouseId;
if (!warehouseMap[key]) {
warehouseMap[key] = {
warehouseId: item.warehouseId,
logicWarehouseName: item.logicWarehouseName,
coilCount: 0,
totalDuration: 0,
totalCost: 0
};
}
warehouseMap[key].coilCount++;
warehouseMap[key].totalDuration += (item.allocationBasisDays || 0);
warehouseMap[key].totalCost += (item.costAmount || 0);
});
this.warehouseSummaryList = Object.values(warehouseMap).map(w => ({
...w,
averageUnitCost: w.coilCount > 0 ? (w.totalCost / w.coilCount).toFixed(2) : 0
}));
} else if (this.queryParams.dimension === 'time') {
// 按时间聚合
const timeMap = {};
data.forEach(item => {
const date = item.calcDate;
if (!timeMap[date]) {
timeMap[date] = {
date: date,
coilCount: 0,
totalDuration: 0,
totalCost: 0
};
}
timeMap[date].coilCount++;
timeMap[date].totalDuration += (item.allocationBasisDays || 0);
timeMap[date].totalCost += (item.costAmount || 0);
});
this.timeSummaryList = Object.values(timeMap).map(t => ({
...t,
averageUnitCost: t.coilCount > 0 ? (t.totalCost / t.coilCount).toFixed(2) : 0
}));
} else if (this.queryParams.dimension === 'energy') {
// 按能源类型聚合
const energyMap = {};
data.forEach(item => {
const key = item.energyTypeId;
if (!energyMap[key]) {
energyMap[key] = {
energyTypeId: item.energyTypeId,
energyTypeName: item.energyTypeName,
totalConsumption: 0,
totalCost: 0
};
}
energyMap[key].totalConsumption += (item.consumptionQty || 0);
energyMap[key].totalCost += (item.costAmount || 0);
});
const totalCost = Object.values(energyMap).reduce((sum, e) => sum + e.totalCost, 0);
this.energySummaryList = Object.values(energyMap).map(e => ({
...e,
percentage: totalCost > 0 ? ((e.totalCost / totalCost) * 100).toFixed(2) : 0,
averageRate: e.totalConsumption > 0 ? (e.totalCost / e.totalConsumption).toFixed(4) : 0
}));
}
},
loadMockData() {
setTimeout(() => {
this.coilSummaryList = [
{
coilCode: 'COIL-20231201-001',
logicWarehouseName: '库区A',
startTime: '2023-12-01 08:00:00',
endTime: '2023-12-01 16:30:00',
duration: 8.5,
totalCost: 1250.50,
unitCost: 147.12
},
{
coilCode: 'COIL-20231201-002',
logicWarehouseName: '库区A',
startTime: '2023-12-01 17:00:00',
endTime: '2023-12-02 01:30:00',
duration: 8.5,
totalCost: 1248.75,
unitCost: 146.88
}
];
this.warehouseSummaryList = [
{
logicWarehouseName: '库区A',
coilCount: 45,
totalDuration: 382.5,
totalCost: 56325.50,
averageUnitCost: 1251.67
},
{
logicWarehouseName: '库区B',
coilCount: 38,
totalDuration: 323.0,
totalCost: 47850.25,
averageUnitCost: 1259.22
}
];
this.timeSummaryList = [
{
date: '2023-12-01',
coilCount: 12,
totalDuration: 102.0,
totalCost: 15125.50,
averageUnitCost: 1260.46
},
{
date: '2023-12-02',
coilCount: 15,
totalDuration: 127.5,
totalCost: 18950.75,
averageUnitCost: 1263.38
},
{
date: '2023-12-03',
coilCount: 18,
totalDuration: 153.0,
totalCost: 22750.25,
averageUnitCost: 1263.91
}
];
this.energySummaryList = [
{
energyTypeId: 1,
energyTypeName: '电',
totalConsumption: 32500,
consumptionUnit: 'kWh',
totalCost: 45875,
percentage: 81.45,
averageRate: 1.4115
},
{
energyTypeId: 2,
energyTypeName: '水',
totalConsumption: 4200,
consumptionUnit: '吨',
totalCost: 8400,
percentage: 14.91,
averageRate: 2.0
},
{
energyTypeId: 3,
energyTypeName: '气',
totalConsumption: 1550,
consumptionUnit: '立方米',
totalCost: 2050.5,
percentage: 3.64,
averageRate: 1.3229
}
];
this.$nextTick(() => {
this.initCharts();
});
}, 500);
},
resetQuery() {
this.queryParams = {
dimension: 'coil',
startDate: '',
endDate: ''
};
this.handleQuery();
},
initCharts() {
if (this.queryParams.dimension === 'time') {
this.initCostTrendChart();
} else if (this.queryParams.dimension === 'energy') {
this.initEnergyCostPieChart();
}
},
initCostTrendChart() {
const chartDom = document.getElementById('costTrendChart');
if (!chartDom) return;
const instance = echarts.init(chartDom);
this.chartInstances.trend = instance;
const option = {
tooltip: {
trigger: 'axis'
},
legend: {
data: ['总成本', '平均单位成本']
},
xAxis: {
type: 'category',
data: this.timeSummaryList.map(item => item.date)
},
yAxis: [
{
type: 'value',
name: '总成本',
axisLabel: {
formatter: '¥{value}'
}
},
{
type: 'value',
name: '平均单位成本',
axisLabel: {
formatter: '¥{value}'
}
}
],
series: [
{
name: '总成本',
type: 'bar',
data: this.timeSummaryList.map(item => item.totalCost),
yAxisIndex: 0
},
{
name: '平均单位成本',
type: 'line',
data: this.timeSummaryList.map(item => item.averageUnitCost),
yAxisIndex: 1
}
]
};
instance.setOption(option);
},
initEnergyCostPieChart() {
const chartDom = document.getElementById('energyCostPieChart');
if (!chartDom) return;
const instance = echarts.init(chartDom);
this.chartInstances.energy = instance;
const option = {
tooltip: {
trigger: 'item',
formatter: '{b}: ¥{c} ({d}%)'
},
legend: {
orient: 'vertical',
left: 'left'
},
series: [
{
name: '能源成本',
type: 'pie',
radius: '50%',
data: this.energySummaryList.map(item => ({
value: item.totalCost,
name: item.energyTypeName
})),
emphasis: {
itemStyle: {
shadowBlur: 10,
shadowOffsetX: 0,
shadowColor: 'rgba(0, 0, 0, 0.5)'
}
}
}
]
};
instance.setOption(option);
},
formatTime(time) {
if (!time) return '-';
return new Date(time).toLocaleString('zh-CN');
},
formatNumber(value, decimals = 2) {
if (value === null || value === undefined) return '0.00';
return parseFloat(value).toFixed(decimals);
},
viewCoilDetail(row) {
this.$router.push({
path: '/ems/cost/coilCost',
query: { coilId: row.coilId }
});
},
viewWarehouseDetail(row) {
this.$router.push({
path: '/ems/cost/warehouseProduction',
query: { logicWarehouseId: row.logicWarehouseId }
});
}
},
beforeDestroy() {
Object.values(this.chartInstances).forEach(instance => {
instance?.dispose();
});
}
};
</script>
<style scoped lang="scss">
.energy-cost-summary-page {
padding: 20px;
.search-card {
margin-bottom: 20px;
}
.chart-card,
.summary-card {
margin-bottom: 20px;
.card-title {
font-size: 16px;
font-weight: bold;
color: #303133;
}
}
.cost-value {
color: #f56c6c;
font-weight: bold;
}
}
</style>

View File

@@ -0,0 +1,367 @@
<template>
<div class="energy-cost-page">
<section class="filter-card">
<el-form :inline="true" :model="filters" size="small">
<el-form-item label="日期范围">
<el-date-picker
v-model="dateRange"
type="daterange"
range-separator=""
start-placeholder="开始日期"
end-placeholder="结束日期"
value-format="yyyy-MM-dd"
@change="handleDateChange"
/>
</el-form-item>
<el-form-item label="能源类型">
<el-select v-model="filters.energyTypeId" placeholder="全部" clearable @change="refreshAll">
<el-option v-for="item in energyTypeList" :key="item.energyTypeId" :label="item.name" :value="item.energyTypeId" />
</el-select>
</el-form-item>
<el-form-item label="库区">
<el-select v-model="filters.warehouseId" placeholder="全部" clearable filterable @change="refreshAll">
<el-option v-for="item in warehouseList" :key="item.warehouseId" :label="item.warehouseName" :value="item.warehouseId" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="el-icon-search" @click="refreshAll">查询</el-button>
<el-button icon="el-icon-refresh" @click="resetFilter">重置</el-button>
</el-form-item>
</el-form>
</section>
<section class="overview-grid">
<div class="metal-card" v-for="item in overviewCards" :key="item.key">
<div class="card-label">{{ item.label }}</div>
<div class="card-value">{{ item.value }}</div>
<div class="card-sub">{{ item.sub }}</div>
</div>
</section>
<section class="content-grid">
<div class="metal-panel">
<div class="panel-header">
<div class="panel-title">能源成本汇总</div>
<el-radio-group v-model="groupBy" size="small" @change="loadSummary">
<el-radio-button label="energyType">能源</el-radio-button>
<el-radio-button label="warehouse">库区</el-radio-button>
<el-radio-button label="meter">仪表</el-radio-button>
<el-radio-button label="task">任务</el-radio-button>
</el-radio-group>
</div>
<el-table
:data="summaryList"
height="300px"
stripe
v-loading="summaryLoading"
>
<el-table-column label="分组" prop="groupName" min-width="120" />
<el-table-column label="能耗 (kWh)" prop="totalConsumption" min-width="120">
<template slot-scope="scope">{{ formatNumber(scope.row.totalConsumption) }}</template>
</el-table-column>
<el-table-column label="费用 (元)" prop="totalCost" min-width="120">
<template slot-scope="scope">{{ formatNumber(scope.row.totalCost) }}</template>
</el-table-column>
<el-table-column label="钢卷数" prop="coilCount" min-width="100" />
</el-table>
</div>
<div class="metal-panel">
<div class="panel-header">
<div class="panel-title">能源明细</div>
<div class="panel-actions">
<el-input v-model="detailFilters.currentCoilNo" placeholder="当前卷号" size="small" clearable @keyup.enter.native="loadDetail" />
<el-button size="small" type="primary" @click="loadDetail">刷新</el-button>
</div>
</div>
<el-table
:data="detailList"
height="300px"
stripe
v-loading="detailLoading"
>
<el-table-column label="日期" prop="calcDate" min-width="110" />
<el-table-column label="当前卷号" prop="currentCoilNo" min-width="140" />
<el-table-column label="能源类型" prop="energyTypeId" min-width="100" />
<el-table-column label="能耗" prop="consumptionQty" min-width="100">
<template slot-scope="scope">{{ formatNumber(scope.row.consumptionQty) }}</template>
</el-table-column>
<el-table-column label="费用 (元)" prop="costAmount" min-width="110">
<template slot-scope="scope">{{ formatNumber(scope.row.costAmount) }}</template>
</el-table-column>
<el-table-column label="分摊系数" prop="allocationFactor" min-width="110">
<template slot-scope="scope">{{ toPercent(scope.row.allocationFactor) }}</template>
</el-table-column>
</el-table>
<pagination
v-show="detailTotal > 0"
:total="detailTotal"
:page.sync="detailFilters.pageNum"
:limit.sync="detailFilters.pageSize"
@pagination="loadDetail"
/>
</div>
</section>
</div>
</template>
<script>
import { fetchEnergyOverview, fetchEnergySummary, fetchEnergyDetail } from '@/api/ems/energyCostReport'
import { listEnergyType } from '@/api/ems/energyType'
import { listWarehouse } from '@/api/wms/warehouse'
export default {
name: 'EnergyCostDashboard',
data() {
return {
filters: {
startDate: '',
endDate: '',
energyTypeId: undefined,
warehouseId: undefined
},
dateRange: [],
energyTypeList: [],
warehouseList: [],
overview: {
coilCount: 0,
totalConsumption: 0,
totalCost: 0
},
summaryList: [],
summaryLoading: false,
groupBy: 'energyType',
detailList: [],
detailTotal: 0,
detailLoading: false,
detailFilters: {
pageNum: 1,
pageSize: 10,
currentCoilNo: ''
}
}
},
computed: {
overviewCards() {
return [
{
key: 'coil',
label: '钢卷数量',
value: this.formatNumber(this.overview.coilCount),
sub: 'Coils involved'
},
{
key: 'consumption',
label: '总能耗 (kWh)',
value: this.formatNumber(this.overview.totalConsumption),
sub: 'Total consumption'
},
{
key: 'cost',
label: '总费用 (元)',
value: '¥' + this.formatNumber(this.overview.totalCost),
sub: 'Total cost'
}
]
}
},
created() {
this.initMeta()
},
methods: {
initMeta() {
listEnergyType({ pageNum: 1, pageSize: 999 }).then(res => {
this.energyTypeList = res.rows || []
})
listWarehouse().then(res => {
this.warehouseList = res.data || []
})
this.refreshAll()
},
handleDateChange(range) {
if (range && range.length === 2) {
this.filters.startDate = range[0]
this.filters.endDate = range[1]
} else {
this.filters.startDate = ''
this.filters.endDate = ''
}
this.refreshAll()
},
resetFilter() {
this.dateRange = []
this.filters = {
startDate: '',
endDate: '',
energyTypeId: undefined,
warehouseId: undefined
}
this.detailFilters.currentCoilNo = ''
this.refreshAll()
},
refreshAll() {
this.loadOverview()
this.loadSummary()
this.detailFilters.pageNum = 1
this.loadDetail()
},
loadOverview() {
fetchEnergyOverview(this.mergeFilters()).then(res => {
this.overview = res.data || { coilCount: 0, totalConsumption: 0, totalCost: 0 }
})
},
loadSummary() {
this.summaryLoading = true
fetchEnergySummary({ ...this.mergeFilters(), groupBy: this.groupBy }).then(res => {
this.summaryList = res.data || []
}).finally(() => {
this.summaryLoading = false
})
},
loadDetail() {
this.detailLoading = true
const params = {
...this.mergeFilters(),
...this.detailFilters
}
fetchEnergyDetail(params).then(res => {
this.detailList = res.rows || []
this.detailTotal = res.total || 0
}).finally(() => {
this.detailLoading = false
})
},
mergeFilters() {
const { startDate, endDate, energyTypeId, warehouseId } = this.filters
return {
startDate,
endDate,
energyTypeId,
warehouseId
}
},
formatNumber(val) {
if (val === undefined || val === null) return '-'
const num = Number(val)
if (Number.isNaN(num)) return val
return num.toLocaleString(undefined, { maximumFractionDigits: 2 })
},
toPercent(val) {
if (!val) return '0%'
return (Number(val) * 100).toFixed(2) + '%'
}
}
}
</script>
<style lang="scss" scoped>
.energy-cost-page {
display: flex;
flex-direction: column;
gap: 16px;
padding: 16px;
background: #0f131a;
min-height: calc(100vh - 100px);
}
.filter-card {
background: linear-gradient(135deg, #1f242b, #181c23);
border: 1px solid #2f353f;
border-radius: 10px;
padding: 16px;
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.05);
}
.overview-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap: 16px;
}
.metal-card {
background: linear-gradient(160deg, #2b3038, #1c1f27);
border: 1px solid #3c434f;
border-radius: 12px;
padding: 16px;
color: #d9dee9;
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.35);
min-height: 120px;
display: flex;
flex-direction: column;
justify-content: center;
}
.card-label {
font-size: 14px;
color: #8f96a3;
text-transform: uppercase;
}
.card-value {
margin-top: 8px;
font-size: 28px;
font-weight: 600;
color: #f6f7fb;
}
.card-sub {
margin-top: 4px;
font-size: 12px;
color: #707684;
}
.content-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(420px, 1fr));
gap: 16px;
}
.metal-panel {
background: #1b1f27;
border: 1px solid #303641;
border-radius: 12px;
padding: 16px;
box-shadow: 0 12px 25px rgba(0, 0, 0, 0.35);
display: flex;
flex-direction: column;
min-height: 380px;
}
.panel-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
}
.panel-title {
font-size: 16px;
color: #f0f3ff;
letter-spacing: 0.5px;
}
.panel-actions {
display: flex;
gap: 8px;
align-items: center;
}
.el-table {
background: transparent;
color: #dfe3ee;
}
.el-table th,
.el-table td {
background-color: transparent !important;
}
.el-table::before {
background-color: transparent;
}
.pagination {
margin-top: 10px;
align-self: flex-end;
}
</style>

View File

@@ -0,0 +1,540 @@
<template>
<div class="warehouse-production-page">
<!-- 查询条件 -->
<el-card class="search-card">
<el-form :model="queryParams" label-width="120px" size="small">
<el-row :gutter="20">
<el-col :xs="24" :sm="12" :md="6">
<el-form-item label="逻辑库区:">
<el-select v-model="queryParams.logicWarehouseId" placeholder="请选择库区" clearable @change="handleQuery">
<el-option v-for="item in warehouseList" :key="item.warehouseId" :label="item.warehouseName" :value="item.warehouseId"></el-option>
</el-select>
</el-form-item>
</el-col>
<el-col :xs="24" :sm="12" :md="6">
<el-form-item label="开始日期:">
<el-date-picker v-model="queryParams.startDate" type="date" placeholder="选择开始日期" value-format="yyyy-MM-dd" @change="handleQuery"></el-date-picker>
</el-form-item>
</el-col>
<el-col :xs="24" :sm="12" :md="6">
<el-form-item label="结束日期:">
<el-date-picker v-model="queryParams.endDate" type="date" placeholder="选择结束日期" value-format="yyyy-MM-dd" @change="handleQuery"></el-date-picker>
</el-form-item>
</el-col>
<el-col :xs="24" :sm="12" :md="6">
<el-button type="primary" icon="el-icon-search" size="small" @click="handleQuery">查询</el-button>
<el-button icon="el-icon-refresh" size="small" @click="resetQuery">重置</el-button>
</el-col>
</el-row>
</el-form>
</el-card>
<!-- 统计信息卡片 -->
<el-row :gutter="20" class="statistics-row">
<el-col :xs="24" :sm="12" :md="6">
<div class="stat-card">
<div class="stat-label">生产钢卷数</div>
<div class="stat-value">{{ statistics.coilCount }}</div>
<div class="stat-unit"></div>
</div>
</el-col>
<el-col :xs="24" :sm="12" :md="6">
<div class="stat-card">
<div class="stat-label">总生产时长</div>
<div class="stat-value">{{ formatNumber(statistics.totalDuration, 1) }}</div>
<div class="stat-unit">小时</div>
</div>
</el-col>
<el-col :xs="24" :sm="12" :md="6">
<div class="stat-card">
<div class="stat-label">总能源成本</div>
<div class="stat-value">¥ {{ formatNumber(statistics.totalCost, 2) }}</div>
<div class="stat-unit"></div>
</div>
</el-col>
<el-col :xs="24" :sm="12" :md="6">
<div class="stat-card highlight">
<div class="stat-label">单位成本</div>
<div class="stat-value">¥ {{ formatNumber(statistics.unitCost, 2) }}</div>
<div class="stat-unit">/</div>
</div>
</el-col>
</el-row>
<!-- 能源成本分解 -->
<el-row :gutter="20">
<el-col :xs="24" :md="12">
<el-card class="chart-card" v-loading="loading">
<div slot="header" class="clearfix">
<span class="card-title">能源成本占比</span>
</div>
<div id="energyCostPieChart" style="height: 350px;"></div>
</el-card>
</el-col>
<el-col :xs="24" :md="12">
<el-card class="chart-card" v-loading="loading">
<div slot="header" class="clearfix">
<span class="card-title">能源消耗占比</span>
</div>
<div id="energyConsumptionPieChart" style="height: 350px;"></div>
</el-card>
</el-col>
</el-row>
<!-- 能源成本明细表 -->
<el-card class="detail-card" v-loading="loading">
<div slot="header" class="clearfix">
<span class="card-title">能源成本明细</span>
</div>
<el-table :data="energyBreakdown" stripe border>
<el-table-column prop="energyTypeName" label="能源类型" width="150"></el-table-column>
<el-table-column prop="consumption" label="总消耗量" width="120">
<template slot-scope="scope">
{{ formatNumber(scope.row.consumption, 2) }}
</template>
</el-table-column>
<el-table-column prop="consumptionUnit" label="单位" width="100"></el-table-column>
<el-table-column prop="cost" label="总成本" width="120">
<template slot-scope="scope">
<span class="cost-value">¥ {{ formatNumber(scope.row.cost, 2) }}</span>
</template>
</el-table-column>
<el-table-column prop="percentage" label="成本占比" width="120">
<template slot-scope="scope">
{{ formatNumber(scope.row.percentage, 2) }}%
</template>
</el-table-column>
<el-table-column prop="averageUnitCost" label="单位成本" width="120">
<template slot-scope="scope">
¥ {{ formatNumber(scope.row.averageUnitCost, 4) }}/单位
</template>
</el-table-column>
</el-table>
</el-card>
<!-- 钢卷列表 -->
<el-card class="coil-list-card" v-loading="loading">
<div slot="header" class="clearfix">
<span class="card-title">该库区生产的钢卷列表</span>
<span class="count">( {{ coilList.length }} )</span>
</div>
<el-table :data="coilList" stripe border max-height="500">
<el-table-column prop="coilCode" label="钢卷编号" width="150"></el-table-column>
<el-table-column prop="startTime" label="开始时间" width="180">
<template slot-scope="scope">
{{ formatTime(scope.row.startTime) }}
</template>
</el-table-column>
<el-table-column prop="endTime" label="完成时间" width="180">
<template slot-scope="scope">
{{ formatTime(scope.row.endTime) }}
</template>
</el-table-column>
<el-table-column prop="duration" label="生产时长" width="100">
<template slot-scope="scope">
{{ formatNumber(scope.row.duration, 1) }}h
</template>
</el-table-column>
<el-table-column prop="totalCost" label="能源成本" width="120">
<template slot-scope="scope">
<span class="cost-value">¥ {{ formatNumber(scope.row.totalCost, 2) }}</span>
</template>
</el-table-column>
<el-table-column label="操作" width="100" fixed="right">
<template slot-scope="scope">
<el-button type="primary" size="mini" @click="viewCoilDetail(scope.row)">查看详情</el-button>
</template>
</el-table-column>
</el-table>
</el-card>
</div>
</template>
<script>
import * as echarts from 'echarts'
import { listEnergyCoilDaily, listWarehouse } from '@/api/ems/energyAllocation'
export default {
name: 'WarehouseProduction',
data() {
return {
loading: false,
queryParams: {
logicWarehouseId: '',
startDate: '',
endDate: ''
},
warehouseList: [],
statistics: {
coilCount: 0,
totalDuration: 0,
totalConsumption: 0,
totalCost: 0,
unitCost: 0
},
energyBreakdown: [],
coilList: [],
chartInstances: {}
};
},
mounted() {
this.loadWarehouseList();
this.handleQuery();
},
methods: {
loadWarehouseList() {
// 调用后端API获取库区列表
listWarehouse().then(response => {
this.warehouseList = response.data || [];
if (this.warehouseList.length === 0) {
this.$message.warning('暂无库区数据');
}
}).catch(() => {
this.$message.error('加载库区列表失败');
// 加载模拟数据作为备用
this.warehouseList = [
{ id: 1, name: '库区A' },
{ id: 2, name: '库区B' },
{ id: 3, name: '库区C' }
];
});
},
handleQuery() {
this.loading = true;
// 调用后端API获取库区生产统计
listEnergyCoilDaily(this.queryParams).then(response => {
const data = response.rows || [];
this.processWarehouseData(data);
this.initCharts();
}).catch(() => {
this.$message.error('加载库区生产统计失败');
// 加载模拟数据作为备用
this.loadMockData();
}).finally(() => {
this.loading = false;
});
},
processWarehouseData(data) {
if (!data || data.length === 0) {
this.statistics = {
coilCount: 0,
totalDuration: 0,
totalConsumption: 0,
totalCost: 0,
unitCost: 0
};
this.energyBreakdown = [];
this.coilList = [];
return;
}
// 统计基本信息
const coilCount = new Set(data.map(d => d.coilId)).size;
const totalDuration = data.reduce((sum, d) => sum + (d.allocationBasisDays || 0), 0);
const totalCost = data.reduce((sum, d) => sum + (d.costAmount || 0), 0);
const totalConsumption = data.reduce((sum, d) => sum + (d.consumptionQty || 0), 0);
this.statistics = {
coilCount: coilCount,
totalDuration: totalDuration.toFixed(2),
totalConsumption: totalConsumption.toFixed(2),
totalCost: totalCost.toFixed(2),
unitCost: coilCount > 0 ? (totalCost / coilCount).toFixed(2) : 0
};
// 按能源类型聚合
const energyMap = {};
data.forEach(item => {
const key = item.energyTypeId;
if (!energyMap[key]) {
energyMap[key] = {
energyTypeId: item.energyTypeId,
energyTypeName: item.energyTypeName,
consumption: 0,
consumptionUnit: item.consumptionUnit || '单位',
cost: 0
};
}
energyMap[key].consumption += (item.consumptionQty || 0);
energyMap[key].cost += (item.costAmount || 0);
});
this.energyBreakdown = Object.values(energyMap).map(e => ({
...e,
percentage: totalCost > 0 ? ((e.cost / totalCost) * 100).toFixed(2) : 0,
averageUnitCost: e.consumption > 0 ? (e.cost / e.consumption).toFixed(4) : 0
}));
// 按钢卷聚合
const coilMap = {};
data.forEach(item => {
const key = item.coilId;
if (!coilMap[key]) {
coilMap[key] = {
coilId: item.coilId,
coilCode: item.currentCoilNo || item.enterCoilNo,
startTime: item.calcDate,
endTime: item.calcDate,
duration: item.allocationBasisDays || 0,
totalCost: 0
};
}
coilMap[key].totalCost += (item.costAmount || 0);
});
this.coilList = Object.values(coilMap);
},
loadMockData() {
setTimeout(() => {
this.statistics = {
coilCount: 45,
totalDuration: 382.5,
totalConsumption: 38250,
totalCost: 56325.50,
unitCost: 1251.67
};
this.energyBreakdown = [
{
energyTypeId: 1,
energyTypeName: '电',
consumption: 32500,
consumptionUnit: 'kWh',
cost: 45875,
percentage: 81.45,
averageUnitCost: 1.4115
},
{
energyTypeId: 2,
energyTypeName: '水',
consumption: 4200,
consumptionUnit: '吨',
cost: 8400,
percentage: 14.91,
averageUnitCost: 2.0
},
{
energyTypeId: 3,
energyTypeName: '气',
consumption: 1550,
consumptionUnit: '立方米',
cost: 2050.5,
percentage: 3.64,
averageUnitCost: 1.3229
}
];
this.coilList = [
{
coilCode: 'COIL-20231201-001',
startTime: '2023-12-01 08:00:00',
endTime: '2023-12-01 16:30:00',
duration: 8.5,
totalCost: 1250.50
},
{
coilCode: 'COIL-20231201-002',
startTime: '2023-12-01 17:00:00',
endTime: '2023-12-02 01:30:00',
duration: 8.5,
totalCost: 1248.75
},
{
coilCode: 'COIL-20231202-001',
startTime: '2023-12-02 08:00:00',
endTime: '2023-12-02 16:15:00',
duration: 8.25,
totalCost: 1205.30
}
];
this.$nextTick(() => {
this.initCharts();
});
}, 500);
},
resetQuery() {
this.queryParams = {
logicWarehouseId: '',
startDate: '',
endDate: ''
};
this.handleQuery();
},
initCharts() {
this.initCostPieChart();
this.initConsumptionPieChart();
},
initCostPieChart() {
const chartDom = document.getElementById('energyCostPieChart');
if (!chartDom) return;
const instance = echarts.init(chartDom);
this.chartInstances.cost = instance;
const option = {
tooltip: {
trigger: 'item',
formatter: '{b}: ¥{c} ({d}%)'
},
legend: {
orient: 'vertical',
left: 'left'
},
series: [
{
name: '能源成本',
type: 'pie',
radius: '50%',
data: this.energyBreakdown.map(item => ({
value: item.cost,
name: item.energyTypeName
})),
emphasis: {
itemStyle: {
shadowBlur: 10,
shadowOffsetX: 0,
shadowColor: 'rgba(0, 0, 0, 0.5)'
}
}
}
]
};
instance.setOption(option);
},
initConsumptionPieChart() {
const chartDom = document.getElementById('energyConsumptionPieChart');
if (!chartDom) return;
const instance = echarts.init(chartDom);
this.chartInstances.consumption = instance;
const option = {
tooltip: {
trigger: 'item',
formatter: '{b}: {c} ({d}%)'
},
legend: {
orient: 'vertical',
left: 'left'
},
series: [
{
name: '能源消耗',
type: 'pie',
radius: '50%',
data: this.energyBreakdown.map(item => ({
value: item.consumption,
name: item.energyTypeName
})),
emphasis: {
itemStyle: {
shadowBlur: 10,
shadowOffsetX: 0,
shadowColor: 'rgba(0, 0, 0, 0.5)'
}
}
}
]
};
instance.setOption(option);
},
formatTime(time) {
if (!time) return '-';
return new Date(time).toLocaleString('zh-CN');
},
formatNumber(value, decimals = 2) {
if (value === null || value === undefined) return '0.00';
return parseFloat(value).toFixed(decimals);
},
viewCoilDetail(row) {
this.$router.push({
path: '/ems/cost/coilCost',
query: { coilId: row.coilId }
});
}
},
beforeDestroy() {
Object.values(this.chartInstances).forEach(instance => {
instance?.dispose();
});
}
};
</script>
<style scoped lang="scss">
.warehouse-production-page {
padding: 20px;
.search-card {
margin-bottom: 20px;
}
.statistics-row {
margin-bottom: 20px;
.stat-card {
background: #ffffff;
border: 1px solid #ebeef5;
border-radius: 4px;
padding: 20px;
text-align: center;
transition: all 0.3s ease;
&:hover {
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
}
&.highlight {
background: linear-gradient(135deg, #f0f9ff 0%, #e0f2fe 100%);
border-color: #409eff;
}
.stat-label {
font-size: 12px;
color: #909399;
margin-bottom: 8px;
}
.stat-value {
font-size: 28px;
font-weight: bold;
color: #303133;
margin-bottom: 4px;
}
.stat-unit {
font-size: 12px;
color: #606266;
}
}
}
.chart-card,
.detail-card,
.coil-list-card {
margin-bottom: 20px;
.card-title {
font-size: 16px;
font-weight: bold;
color: #303133;
}
.count {
font-size: 12px;
color: #909399;
margin-left: 10px;
}
}
.cost-value {
color: #f56c6c;
font-weight: bold;
}
}
</style>

View File

@@ -1,365 +1,14 @@
<template>
<div class="app-container">
<el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="68px">
<!-- <el-form-item label="关联计量设备" prop="meterId">
<el-input
v-model="queryParams.meterId"
placeholder="请输入关联计量设备"
clearable
@keyup.enter.native="handleQuery"
/>
</el-form-item> -->
<el-form-item label="起始读数" prop="startReading">
<el-input
v-model="queryParams.startReading"
placeholder="请输入起始读数"
clearable
@keyup.enter.native="handleQuery"
/>
</el-form-item>
<el-form-item label="结束读数" prop="endReading">
<el-input
v-model="queryParams.endReading"
placeholder="请输入结束读数"
clearable
@keyup.enter.native="handleQuery"
/>
</el-form-item>
<el-form-item label="消耗量" prop="consumption">
<el-input
v-model="queryParams.consumption"
placeholder="请输入消耗量"
clearable
@keyup.enter.native="handleQuery"
/>
</el-form-item>
<el-form-item label="起始时间" prop="startTime">
<el-date-picker clearable
v-model="queryParams.startTime"
type="date"
value-format="yyyy-MM-dd"
placeholder="请选择起始时间">
</el-date-picker>
</el-form-item>
<el-form-item label="结束时间" prop="endTime">
<el-date-picker clearable
v-model="queryParams.endTime"
type="date"
value-format="yyyy-MM-dd"
placeholder="请选择结束时间">
</el-date-picker>
</el-form-item>
<el-form-item label="记录人" prop="recordedBy">
<el-input
v-model="queryParams.recordedBy"
placeholder="请输入记录人"
clearable
@keyup.enter.native="handleQuery"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
<el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button
type="primary"
plain
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-col>
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
<el-table v-loading="loading" :data="energyConsumptionList" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="" align="center" prop="energyConsumptionId" v-if="true"/>
<el-table-column label="计量设备" align="center" prop="meterId" />
<el-table-column label="起始读数" align="center" prop="startReading" />
<el-table-column label="结束读数" align="center" prop="endReading" />
<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} {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} {h}:{i}:{s}') }}</span>
</template>
</el-table-column>
<el-table-column label="记录人" align="center" prop="recordedBy" />
<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>
<pagination
v-show="total>0"
:total="total"
:page.sync="queryParams.pageNum"
:limit.sync="queryParams.pageSize"
@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="meterId">
<el-input v-model="form.meterId" placeholder="请输入关联计量设备" />
</el-form-item>
<el-form-item label="起始读数" prop="startReading">
<el-input v-model="form.startReading" placeholder="请输入起始读数" />
</el-form-item>
<el-form-item label="结束读数" prop="endReading">
<el-input v-model="form.endReading" placeholder="请输入结束读数" />
</el-form-item>
<el-form-item label="消耗量" prop="consumption">
<el-input v-model="form.consumption" placeholder="请输入消耗量" />
</el-form-item>
<el-form-item label="起始时间" prop="startTime">
<el-date-picker clearable
v-model="form.startTime"
type="datetime"
value-format="yyyy-MM-dd HH:mm:ss"
placeholder="请选择起始时间">
</el-date-picker>
</el-form-item>
<el-form-item label="结束时间" prop="endTime">
<el-date-picker clearable
v-model="form.endTime"
type="datetime"
value-format="yyyy-MM-dd HH:mm:ss"
placeholder="请选择结束时间">
</el-date-picker>
</el-form-item>
<el-form-item label="记录人" prop="recordedBy">
<el-input v-model="form.recordedBy" placeholder="请输入记录人" />
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input v-model="form.remark" type="textarea" placeholder="请输入内容" />
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button :loading="buttonLoading" type="primary" @click="submitForm"> </el-button>
<el-button @click="cancel"> </el-button>
</div>
</el-dialog>
</div>
<record />
</template>
<script>
import { listEnergyConsumption, getEnergyConsumption, delEnergyConsumption, addEnergyConsumption, updateEnergyConsumption } from "@/api/ems/energyConsumption";
import Record from './record.vue'
export default {
name: "EnergyConsumption",
data() {
return {
// 按钮loading
buttonLoading: false,
// 遮罩层
loading: true,
// 选中数组
ids: [],
// 非单个禁用
single: true,
// 非多个禁用
multiple: true,
// 显示搜索条件
showSearch: true,
// 总条数
total: 0,
// 能耗记录表格数据
energyConsumptionList: [],
// 弹出层标题
title: "",
// 是否显示弹出层
open: false,
// 查询参数
queryParams: {
pageNum: 1,
pageSize: 10,
meterId: undefined,
startReading: undefined,
endReading: undefined,
consumption: undefined,
startTime: undefined,
endTime: undefined,
recordedBy: undefined,
},
// 表单参数
form: {},
// 表单校验
rules: {
}
};
},
created() {
this.getList();
},
methods: {
/** 查询能耗记录列表 */
getList() {
this.loading = true;
listEnergyConsumption(this.queryParams).then(response => {
this.energyConsumptionList = response.rows;
this.total = response.total;
this.loading = false;
});
},
// 取消按钮
cancel() {
this.open = false;
this.reset();
},
// 表单重置
reset() {
this.form = {
energyConsumptionId: undefined,
meterId: undefined,
startReading: undefined,
endReading: undefined,
consumption: undefined,
startTime: undefined,
endTime: undefined,
recordedBy: undefined,
createBy: undefined,
updateBy: undefined,
createTime: undefined,
updateTime: undefined,
delFlag: undefined,
remark: undefined
};
this.resetForm("form");
},
/** 搜索按钮操作 */
handleQuery() {
this.queryParams.pageNum = 1;
this.getList();
},
/** 重置按钮操作 */
resetQuery() {
this.resetForm("queryForm");
this.handleQuery();
},
// 多选框选中数据
handleSelectionChange(selection) {
this.ids = selection.map(item => item.energyConsumptionId)
this.single = selection.length!==1
this.multiple = !selection.length
},
/** 新增按钮操作 */
handleAdd() {
this.reset();
this.open = true;
this.title = "添加能耗记录";
},
/** 修改按钮操作 */
handleUpdate(row) {
this.loading = true;
this.reset();
const energyConsumptionId = row.energyConsumptionId || this.ids
getEnergyConsumption(energyConsumptionId).then(response => {
this.loading = false;
this.form = response.data;
this.open = true;
this.title = "修改能耗记录";
});
},
/** 提交按钮 */
submitForm() {
this.$refs["form"].validate(valid => {
if (valid) {
this.buttonLoading = true;
if (this.form.energyConsumptionId != null) {
updateEnergyConsumption(this.form).then(response => {
this.$modal.msgSuccess("修改成功");
this.open = false;
this.getList();
}).finally(() => {
this.buttonLoading = false;
});
} else {
addEnergyConsumption(this.form).then(response => {
this.$modal.msgSuccess("新增成功");
this.open = false;
this.getList();
}).finally(() => {
this.buttonLoading = false;
});
}
}
});
},
/** 删除按钮操作 */
handleDelete(row) {
const energyConsumptionIds = row.energyConsumptionId || this.ids;
this.$modal.confirm('是否确认删除能耗记录编号为"' + energyConsumptionIds + '"的数据项?').then(() => {
this.loading = true;
return delEnergyConsumption(energyConsumptionIds);
}).then(() => {
this.loading = false;
this.getList();
this.$modal.msgSuccess("删除成功");
}).catch(() => {
}).finally(() => {
this.loading = false;
});
},
/** 导出按钮操作 */
handleExport() {
this.download('ems/energyConsumption/export', {
...this.queryParams
}, `energyConsumption_${new Date().getTime()}.xlsx`)
}
name: 'EnergyData',
components: {
Record
}
};
}
</script>

View File

@@ -0,0 +1,420 @@
<template>
<div class="energy-consumption-manage">
<!-- 统计卡片 -->
<el-row :gutter="20" class="statistics-row" v-loading="statsLoading">
<el-col :xs="24" :sm="12" :md="6">
<div class="stat-card">
<div class="stat-label">总记录数</div>
<div class="stat-value">{{ statistics.totalCount || 0 }}</div>
<div class="stat-unit"></div>
</div>
</el-col>
<el-col :xs="24" :sm="12" :md="6">
<div class="stat-card">
<div class="stat-label">总消耗量</div>
<div class="stat-value">{{ formatNumber(statistics.totalConsumption, 2) }}</div>
<div class="stat-unit">{{ consumptionUnit }}</div>
</div>
</el-col>
<el-col :xs="24" :sm="12" :md="6">
<div class="stat-card">
<div class="stat-label">平均消耗量</div>
<div class="stat-value">{{ formatNumber(statistics.avgConsumption, 2) }}</div>
<div class="stat-unit">{{ consumptionUnit }}</div>
</div>
</el-col>
<el-col :xs="24" :sm="12" :md="6">
<div class="stat-card">
<div class="stat-label">最大消耗量</div>
<div class="stat-value max-value">{{ formatNumber(statistics.maxConsumption, 2) }}</div>
<div class="stat-unit">{{ consumptionUnit }}</div>
</div>
</el-col>
</el-row>
<!-- 搜索面板 -->
<el-collapse-transition>
<div class="search-panel" v-show="showSearch">
<el-form :model="queryParams" label-width="100px" size="small">
<el-row :gutter="20">
<el-col :xs="24" :sm="12" :md="6">
<el-form-item label="能源类型">
<el-select v-model="queryParams.energyTypeId" placeholder="请选择能源类型" clearable @change="handleQuery">
<el-option v-for="item in energyTypeList" :key="item.energyTypeId" :label="item.name" :value="item.energyTypeId" />
</el-select>
</el-form-item>
</el-col>
<el-col :xs="24" :sm="12" :md="6">
<el-form-item label="起始时间">
<el-date-picker v-model="queryParams.startTime" type="date" placeholder="选择起始时间" value-format="yyyy-MM-dd" />
</el-form-item>
</el-col>
<el-col :xs="24" :sm="12" :md="6">
<el-form-item label="结束时间">
<el-date-picker v-model="queryParams.endTime" type="date" placeholder="选择结束时间" value-format="yyyy-MM-dd" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :xs="24">
<el-form-item>
<el-button type="primary" icon="el-icon-search" size="small" @click="handleQuery">搜索</el-button>
<el-button icon="el-icon-refresh" size="small" @click="resetQuery">重置</el-button>
</el-form-item>
</el-col>
</el-row>
</el-form>
</div>
</el-collapse-transition>
<!-- 工具栏 -->
<div class="toolbar">
<el-button type="primary" icon="el-icon-plus" @click="handleAdd">新增</el-button>
<el-button icon="el-icon-search" @click="showSearch = !showSearch">{{ showSearch ? '隐藏' : '显示' }}搜索</el-button>
</div>
<!-- 数据表格 -->
<el-table :data="tableList" stripe border v-loading="loading" style="width: 100%" max-height="600">
<el-table-column prop="meterCode" label="设备编号" min-width="150" />
<el-table-column prop="energyTypeName" label="能源类型" min-width="120" />
<el-table-column prop="startReading" label="起始读数" min-width="120" align="right">
<template slot-scope="scope">
{{ formatNumber(scope.row.startReading, 2) }}
</template>
</el-table-column>
<el-table-column prop="endReading" label="结束读数" min-width="120" align="right">
<template slot-scope="scope">
{{ formatNumber(scope.row.endReading, 2) }}
</template>
</el-table-column>
<el-table-column prop="consumption" label="消耗量" min-width="120" align="right">
<template slot-scope="scope">
{{ formatNumber(scope.row.consumption, 2) }}
</template>
</el-table-column>
<el-table-column prop="startTime" label="起始时间" min-width="160" :formatter="formatDate" />
<el-table-column prop="endTime" label="结束时间" min-width="160" :formatter="formatDate" />
<el-table-column prop="recordedByName" label="记录人" min-width="100" />
<el-table-column prop="remark" label="备注" min-width="150" show-overflow-tooltip />
<el-table-column label="操作" width="150" fixed="right">
<template slot-scope="scope">
<el-button type="primary" size="small" @click="handleEdit(scope.row)">编辑</el-button>
<el-button type="danger" size="small" @click="handleDelete(scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<el-pagination
@size-change="handlePageSizeChange"
@current-change="handlePageChange"
:current-page="queryParams.pageNum"
:page-sizes="[10, 20, 50, 100]"
:page-size="queryParams.pageSize"
layout="total, sizes, prev, pager, next, jumper"
:total="total"
style="text-align: right; margin-top: 20px"
/>
<!-- 新增/编辑对话框 -->
<el-dialog :title="isEditMode ? '编辑能耗记录' : '新增能耗记录'" :visible.sync="dialogVisible" width="600px" @close="resetForm">
<el-form :model="form" :rules="rules" ref="formRef" label-width="120px">
<el-form-item label="计量设备" prop="meterId">
<el-select v-model="form.meterId" placeholder="请选择计量设备" filterable>
<el-option v-for="item in meterList" :key="item.meterId" :label="`${item.meterCode} (${item.energyTypeName})`" :value="item.meterId" />
</el-select>
</el-form-item>
<el-form-item label="起始读数" prop="startReading">
<el-input-number v-model="form.startReading" :precision="2" :step="0.01" />
</el-form-item>
<el-form-item label="结束读数" prop="endReading">
<el-input-number v-model="form.endReading" :precision="2" :step="0.01" />
</el-form-item>
<el-form-item label="消耗量" prop="consumption">
<el-input-number v-model="form.consumption" :precision="2" :step="0.01" disabled />
</el-form-item>
<el-form-item label="记录人" prop="recordedBy">
<el-input v-model="form.recordedByName" placeholder="记录人名称(自动填充)" disabled />
</el-form-item>
<el-form-item label="起始时间" prop="startTime">
<el-date-picker v-model="form.startTime" type="datetime" placeholder="选择起始时间" />
</el-form-item>
<el-form-item label="结束时间" prop="endTime">
<el-date-picker v-model="form.endTime" type="datetime" placeholder="选择结束时间" />
</el-form-item>
<el-form-item label="备注">
<el-input v-model="form.remark" type="textarea" rows="3" placeholder="请输入备注信息" />
</el-form-item>
</el-form>
<span slot="footer" class="dialog-footer">
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="submitForm">确定</el-button>
</span>
</el-dialog>
</div>
</template>
<script>
import { listEnergyConsumption, getEnergyConsumption, addEnergyConsumption, updateEnergyConsumption, deleteEnergyConsumption, getStatistics } from '@/api/ems/energyConsumption'
export default {
name: 'EnergyConsumptionManage',
data() {
return {
loading: false,
statsLoading: false,
showSearch: true,
dialogVisible: false,
isEditMode: false,
tableList: [],
total: 0,
energyTypeList: [],
meterList: [],
statistics: {
totalCount: 0,
totalConsumption: 0,
avgConsumption: 0,
maxConsumption: 0,
minConsumption: 0
},
consumptionUnit: '单位',
queryParams: {
pageNum: 1,
pageSize: 30,
energyTypeId: undefined,
startTime: undefined,
endTime: undefined
},
form: {
energyConsumptionId: undefined,
meterId: undefined,
startReading: 0,
endReading: 0,
consumption: 0,
recordedBy: undefined,
recordedByName: '',
startTime: undefined,
endTime: undefined,
remark: ''
},
rules: {
meterId: [{ required: true, message: '计量设备不能为空', trigger: 'change' }],
startReading: [{ required: true, message: '起始读数不能为空', trigger: 'blur' }],
endReading: [{ required: true, message: '结束读数不能为空', trigger: 'blur' }],
startTime: [{ required: true, message: '起始时间不能为空', trigger: 'change' }],
endTime: [{ required: true, message: '结束时间不能为空', trigger: 'change' }]
}
}
},
created() {
this.getEnergyTypeList()
this.getMeterList()
this.getList()
this.getStatistics()
},
methods: {
getList() {
this.loading = true
listEnergyConsumption(this.queryParams).then(response => {
this.tableList = response.rows
this.total = response.total
}).finally(() => {
this.loading = false
})
},
getEnergyTypeList() {
// TODO: 调用后端API获取能源类型列表
// 临时使用模拟数据
this.energyTypeList = [
{ energyTypeId: 1, name: '电', unit: 'kWh' },
{ energyTypeId: 2, name: '水', unit: 'm³' },
{ energyTypeId: 3, name: '气', unit: 'm³' }
]
// 初始化单位为第一个能源类型的单位
if (this.energyTypeList.length > 0) {
this.consumptionUnit = this.energyTypeList[0].unit
}
},
getMeterList() {
// TODO: 调用后端API获取计量设备列表
// 临时使用模拟数据
this.meterList = [
{ meterId: 1, meterCode: 'METER001', energyTypeName: '电' },
{ meterId: 2, meterCode: 'METER002', energyTypeName: '水' },
{ meterId: 3, meterCode: 'METER003', energyTypeName: '气' }
]
},
getStatistics() {
this.statsLoading = true
getStatistics(this.queryParams).then(response => {
this.statistics = response.data || {}
}).catch(() => {
this.$message.error('加载统计信息失败')
}).finally(() => {
this.statsLoading = false
})
},
handleQuery() {
this.queryParams.pageNum = 1
// 如果选择了能源类型,更新单位
if (this.queryParams.energyTypeId) {
const selectedType = this.energyTypeList.find(t => t.energyTypeId === this.queryParams.energyTypeId)
if (selectedType) {
this.consumptionUnit = selectedType.unit
}
} else {
// 如果没有选择能源类型,使用第一个的单位
if (this.energyTypeList.length > 0) {
this.consumptionUnit = this.energyTypeList[0].unit
}
}
this.getList()
this.getStatistics()
},
resetQuery() {
this.queryParams = {
pageNum: 1,
pageSize: 30,
energyTypeId: undefined,
startTime: undefined,
endTime: undefined
}
this.getList()
this.getStatistics()
},
handlePageChange(val) {
this.queryParams.pageNum = val
this.getList()
},
handlePageSizeChange(val) {
this.queryParams.pageSize = val
this.queryParams.pageNum = 1
this.getList()
},
handleAdd() {
this.isEditMode = false
this.resetForm()
this.dialogVisible = true
},
handleEdit(row) {
this.isEditMode = true
this.form = JSON.parse(JSON.stringify(row))
this.dialogVisible = true
},
handleDelete(row) {
this.$confirm('确定删除该记录吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
deleteEnergyConsumption(row.energyConsumptionId).then(() => {
this.$message.success('删除成功')
this.getList()
}).catch(() => {
this.$message.error('删除失败')
})
}).catch(() => {})
},
submitForm() {
this.$refs.formRef.validate((valid) => {
if (valid) {
// 计算消耗量
this.form.consumption = this.form.endReading - this.form.startReading
const submitFn = this.isEditMode ? updateEnergyConsumption : addEnergyConsumption
submitFn(this.form).then(() => {
this.$message.success(this.isEditMode ? '编辑成功' : '新增成功')
this.dialogVisible = false
this.getList()
}).catch(() => {
this.$message.error(this.isEditMode ? '编辑失败' : '新增失败')
})
}
})
},
resetForm() {
this.form = {
energyConsumptionId: undefined,
meterId: undefined,
startReading: 0,
endReading: 0,
consumption: 0,
recordedBy: undefined,
recordedByName: '',
startTime: undefined,
endTime: undefined,
remark: ''
}
this.$refs.formRef && this.$refs.formRef.resetFields()
},
formatDate(row, column) {
const date = row[column.property]
return date ? new Date(date).toLocaleString() : '-'
},
formatNumber(value, decimals = 2) {
if (value === null || value === undefined) return '0.00'
return parseFloat(value).toFixed(decimals)
}
}
}
</script>
<style scoped lang="scss">
.energy-consumption-manage {
padding: 20px;
.statistics-row {
margin-bottom: 20px;
.stat-card {
background: #ffffff;
padding: 20px;
border-radius: 10px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
text-align: center;
transition: all 0.3s ease;
&:hover {
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12);
transform: translateY(-2px);
}
.max-value {
color: #409eff;
}
.stat-label {
font-size: 14px;
color: #909399;
margin-bottom: 8px;
}
.stat-value {
font-size: 28px;
font-weight: bold;
color: #303133;
margin: 8px 0;
}
.stat-unit {
font-size: 12px;
color: #909399;
}
}
}
.search-panel {
background: #ffffff;
padding: 16px;
border-radius: 10px;
margin-bottom: 20px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
}
.toolbar {
display: flex;
gap: 10px;
margin-bottom: 20px;
}
}
</style>

View File

@@ -0,0 +1,954 @@
<template>
<div class="energy-consumption-record">
<!-- 筛选面板 -->
<div class="filter-panel">
<el-form :model="filterParams" size="small" :inline="true" label-width="80px">
<el-form-item label="能源类型">
<el-select v-model="filterParams.energyTypeId" placeholder="选择能源类型" clearable @change="handleFilterChange">
<el-option v-for="item in energyTypeList" :key="item.energyTypeId" :label="item.name" :value="item.energyTypeId" />
</el-select>
</el-form-item>
<el-form-item label="库区">
<el-select v-model="filterParams.warehouseId" placeholder="选择库区" clearable @change="handleFilterChange">
<el-option v-for="item in warehouseList" :key="item.warehouseId" :label="item.warehouseName" :value="item.warehouseId" />
</el-select>
</el-form-item>
<el-form-item label="设备编号">
<el-input v-model="filterParams.meterCode" placeholder="搜索设备编号" clearable @input="handleFilterChange" />
</el-form-item>
</el-form>
</div>
<!-- 左右布局 -->
<el-row :gutter="20" class="main-container">
<!-- 左侧设备卡片列表 -->
<el-col :xs="24" :sm="24" :md="12" class="device-column">
<div v-if="allFilteredMeters.length === 0" class="empty-state">
<div class="empty-icon">📦</div>
<div class="empty-text">暂无设备</div>
</div>
<div v-else class="device-list-wrapper">
<el-row :gutter="12" class="device-list">
<el-col
v-for="meter in filteredMeters"
:key="meter.meterId"
:xs="24" :sm="12" :md="8"
>
<div
:class="['device-card', { active: selectedMeter && selectedMeter.meterId === meter.meterId }]"
@click="selectMeter(meter)"
>
<div class="card-header">
<div class="meter-code">{{ meter.meterCode }}</div>
<el-tag size="small" type="info">{{ getEnergyName(meter.energyTypeId) }}</el-tag>
</div>
<div class="card-body">
<div class="info-row">
<span class="label">型号</span>
<span class="value">{{ meter.model || '-' }}</span>
</div>
<div class="info-row">
<span class="label">制造商</span>
<span class="value">{{ meter.manufacturer || '-' }}</span>
</div>
<div class="info-row">
<span class="label">库区</span>
<span class="value">{{ getWarehouseName(meter) }}</span>
</div>
<div class="info-row">
<span class="label">状态</span>
<el-tag :type="getStatusType(meter.status)" size="small">
{{ getStatusText(meter.status) }}
</el-tag>
</div>
</div>
</div>
</el-col>
</el-row>
<!-- 分页 -->
<el-pagination
class="device-pagination"
:current-page="pageParams.pageNum"
:page-size="pageParams.pageSize"
:total="allFilteredMeters.length"
layout="prev, pager, next"
@current-change="handlePageChange"
/>
</div>
</el-col>
<!-- 右侧抄表表单 -->
<el-col :xs="24" :sm="24" :md="12" class="form-column">
<div v-if="selectedMeter" class="record-container">
<!-- 费率信息卡片 -->
<el-card class="rate-info-card" v-if="rateInfo">
<div slot="header" class="clearfix">
<span class="title">费率信息</span>
</div>
<div class="rate-content">
<!-- 固定费率模式 -->
<div v-if="rateInfo.usePeakValley === 0 && rateInfo.useTieredPricing === 0">
<div class="rate-item">
<span class="label">费率类型</span>
<span class="value">固定费率</span>
</div>
<div class="rate-item">
<span class="label">当前费率</span>
<span class="rate-value">{{ rateInfo.rate }}</span>
<span class="unit">{{ rateInfo.currency }}/单位</span>
</div>
</div>
<!-- 峰谷模式 -->
<div v-else-if="rateInfo.usePeakValley === 1 && rateInfo.useTieredPricing === 0">
<div class="rate-item">
<span class="label">费率类型</span>
<span class="value">峰谷分时</span>
</div>
<div class="rate-item">
<span class="label">时段费率</span>
<div class="period-rates">
<span v-for="period in rateInfo.periods" :key="period.periodId" class="period-badge">
{{ getPeriodName(period.periodType) }}: ¥{{ period.rate }}
</span>
</div>
</div>
<div class="rate-item">
<span class="label">平均费率</span>
<span class="rate-value">¥{{ calculateAvgRate(rateInfo.periods) }}</span>
<span class="unit">预计费用按此计算</span>
</div>
</div>
<!-- 梯度模式 -->
<div v-else-if="rateInfo.usePeakValley === 0 && rateInfo.useTieredPricing === 1">
<div class="rate-item">
<span class="label">费率类型</span>
<span class="value">梯度收费</span>
</div>
<div class="rate-item">
<span class="label">梯度费率</span>
<div class="tier-rates">
<span v-for="tier in rateInfo.tiers" :key="tier.tierId" class="tier-badge">
{{ tier.tierLevel }}({{ tier.minUsage }}-{{ tier.maxUsage ? tier.maxUsage : '' }}): ¥{{ tier.rate }}
</span>
</div>
</div>
</div>
<!-- 梯度+峰谷组合模式 -->
<div v-else-if="rateInfo.usePeakValley === 1 && rateInfo.useTieredPricing === 1">
<div class="rate-item">
<span class="label">费率类型</span>
<span class="value">梯度+峰谷组合</span>
</div>
<div class="rate-item">
<span class="label">梯度费率</span>
<div class="tier-rates">
<span v-for="tier in rateInfo.tiers" :key="tier.tierId" class="tier-badge">
{{ tier.tierLevel }}({{ tier.minUsage }}-{{ tier.maxUsage ? tier.maxUsage : '' }}): ¥{{ tier.rate }}
</span>
</div>
</div>
<div class="rate-item">
<span class="label">平均费率</span>
<span class="rate-value">¥{{ calculateAvgRate(rateInfo.tiers) }}</span>
<span class="unit">预计费用按此计算</span>
</div>
</div>
<!-- 本月统计信息 -->
<div class="rate-item" style="border-top: 1px solid #ebeef5; padding-top: 12px; margin-top: 12px;">
<span class="label">本月累计</span>
<span class="rate-value">{{ rateInfo.monthlyConsumption }}</span>
<span class="unit">{{ getEnergyUnit(selectedMeter.energyTypeId) }}</span>
</div>
<!-- 仅在固定费率或梯度模式下显示预估成本 -->
<div v-if="(parseInt(rateInfo.usePeakValley) === 0 && parseInt(rateInfo.useTieredPricing) === 0) || (parseInt(rateInfo.usePeakValley) === 0 && parseInt(rateInfo.useTieredPricing) === 1)" class="rate-item">
<span class="label">本月预估成本</span>
<span class="rate-value">¥{{ rateInfo.monthlyEstimatedCost }}</span>
</div>
<!-- 峰谷或组合模式下显示说明 -->
<div v-else class="rate-item" style="color: #909399; font-size: 12px;">
<span class="label">说明</span>
<span>峰谷模式需知道各时段消耗分布才能准确计算成本</span>
</div>
<!-- 公共信息 -->
<div class="rate-item">
<span class="label">生效日期</span>
<span class="value">{{ formatDate(rateInfo.effectiveDate) }}</span>
</div>
<div class="rate-item">
<span class="label">失效日期</span>
<span class="value">{{ formatDate(rateInfo.expiryDate) }}</span>
</div>
</div>
</el-card>
<!-- 抄表表单 -->
<el-card class="record-form-card">
<div slot="header" class="clearfix">
<span class="title">抄表记录</span>
</div>
<el-form :model="recordForm" :rules="recordRules" ref="recordFormRef" label-width="100px">
<el-form-item label="起始读数" prop="startReading">
<el-input-number
v-model="recordForm.startReading"
:precision="2"
:step="0.01"
:disabled="recordForm.startReading > 0"
@change="onStartReadingChange"
placeholder="请输入起始读数"
/>
<span v-if="recordForm.startReading > 0" class="helper-text">
自动填充自上次抄表的结束读数不可修改
</span>
</el-form-item>
<el-form-item label="结束读数" prop="endReading">
<el-input-number
v-model="recordForm.endReading"
:precision="2"
:step="0.01"
:min="recordForm.startReading"
@change="calculateCost"
placeholder="请输入结束读数"
/>
<span v-if="recordForm.endReading === recordForm.startReading" class="helper-text">
默认为起始读数消耗量为 0
</span>
</el-form-item>
<el-form-item label="消耗量">
<el-input-number v-model="recordForm.consumption" :precision="2" disabled />
</el-form-item>
<el-form-item label="本月预计费用" v-if="recordForm.consumption > 0">
<div class="cost-display">
<span class="cost-value">{{ recordForm.estimatedCost }}</span>
<span class="cost-unit"></span>
</div>
</el-form-item>
<el-form-item label="起始时间" prop="startTime">
<el-date-picker v-model="recordForm.startTime" type="datetime" placeholder="选择起始时间" />
</el-form-item>
<el-form-item label="结束时间" prop="endTime">
<el-date-picker v-model="recordForm.endTime" type="datetime" placeholder="选择结束时间" />
</el-form-item>
<el-form-item label="备注">
<el-input v-model="recordForm.remark" type="textarea" rows="2" placeholder="请输入备注信息" />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="submitRecord">提交抄表</el-button>
<el-button @click="resetRecord">重置</el-button>
</el-form-item>
</el-form>
</el-card>
</div>
<div v-else class="empty-placeholder">
<div class="empty-icon">📋</div>
<div class="empty-text">请在左侧选择设备开始抄表</div>
</div>
</el-col>
</el-row>
</div>
</template>
<script>
import { listMeter } from '@/api/ems/meter'
import { listEnergyType } from '@/api/ems/energyType'
import { listWarehouse } from '@/api/ems/warehouse'
import { listEnergyRate } from '@/api/ems/energyRate'
import { addEnergyConsumption, getLastReading, getMonthlyConsumption } from '@/api/ems/energyConsumption'
export default {
name: 'EnergyConsumptionRecord',
data() {
return {
selectedMeter: null,
meterList: [],
allMeterList: [],
energyTypeList: [],
warehouseList: [],
filterParams: {
energyTypeId: undefined,
warehouseId: undefined,
meterCode: ''
},
// 分页参数
pageParams: {
pageNum: 1,
pageSize: 20
},
totalMeters: 0,
recordForm: {
meterId: undefined,
startReading: 0,
endReading: 0,
consumption: 0,
estimatedCost: 0,
startTime: undefined,
endTime: undefined,
recordedBy: undefined,
remark: ''
},
recordRules: {
startReading: [{ required: true, message: '起始读数不能为空', trigger: 'blur' }],
endReading: [
{ required: true, message: '结束读数不能为空', trigger: 'blur' },
{
validator: (rule, value, callback) => {
if (value < this.recordForm.startReading) {
callback(new Error('结束读数不能小于起始读数'))
} else {
callback()
}
},
trigger: 'blur'
}
],
startTime: [{ required: true, message: '起始时间不能为空', trigger: 'change' }],
endTime: [{ required: true, message: '结束时间不能为空', trigger: 'change' }]
},
rateInfo: null
}
},
computed: {
// 所有过滤后的设备(不分页)
allFilteredMeters() {
return this.allMeterList.filter(meter => {
// 筛选能源类型
if (this.filterParams.energyTypeId && meter.energyTypeId !== this.filterParams.energyTypeId) {
return false
}
// 筛选库区(使用后端返回的库区信息)
if (this.filterParams.warehouseId && meter.warehouseId !== this.filterParams.warehouseId) {
return false
}
// 搜索设备编号
if (this.filterParams.meterCode && !meter.meterCode.includes(this.filterParams.meterCode)) {
return false
}
return true
})
},
// 当前页的设备(分页后)
filteredMeters() {
const start = (this.pageParams.pageNum - 1) * this.pageParams.pageSize
const end = start + this.pageParams.pageSize
return this.allFilteredMeters.slice(start, end)
}
},
created() {
this.getMeterList()
this.getEnergyTypeList()
this.getWarehouseList()
},
methods: {
getMeterList() {
// 只加载第一页,避免一次性加载过多数据
listMeter({ pageNum: 1, pageSize: 20 }).then(response => {
this.allMeterList = response.rows
this.totalMeters = response.total
this.pageParams.pageNum = 1
})
},
getEnergyTypeList() {
// 能源类型通常数量不多,可以全部加载
listEnergyType({ pageNum: 1, pageSize: 100 }).then(response => {
this.energyTypeList = response.rows
})
},
getWarehouseList() {
// 库区通常数量不多,可以全部加载
listWarehouse({ pageNum: 1, pageSize: 100 }).then(response => {
this.warehouseList = response.rows
})
},
getEnergyName(energyTypeId) {
const item = this.energyTypeList.find(e => e.energyTypeId === energyTypeId)
return item ? item.name : '-'
},
getEnergyUnit(energyTypeId) {
const item = this.energyTypeList.find(e => e.energyTypeId === energyTypeId)
return item ? item.unit : '度'
},
getWarehouseName(meter) {
// 直接使用后端返回的库区信息
return meter.warehouseName || '-'
},
getStatusText(status) {
const statusMap = {
0: '在用',
1: '停用',
2: '维护'
}
return statusMap[status] || '未知'
},
getStatusType(status) {
const typeMap = {
0: 'success',
1: 'danger',
2: 'warning'
}
return typeMap[status] || 'info'
},
getPeriodName(periodType) {
const nameMap = {
0: '峰',
1: '谷',
2: '平'
}
return nameMap[periodType] || '未知'
},
calculateAvgRate(items) {
if (!items || items.length === 0) return 0
const sum = items.reduce((total, item) => total + (parseFloat(item.rate) || 0), 0)
return (sum / items.length).toFixed(2)
},
handleFilterChange() {
// 筛选条件变化时,重置分页到第一页
this.pageParams.pageNum = 1
// 如果当前选中的设备不在筛选结果中,则清空选择
if (this.selectedMeter && !this.allFilteredMeters.find(m => m.meterId === this.selectedMeter.meterId)) {
this.selectedMeter = null
this.resetRecord()
}
},
handlePageChange(pageNum) {
this.pageParams.pageNum = pageNum
// 当翻页时,动态加载该页的数据
this.loadMeterPage(pageNum)
},
loadMeterPage(pageNum) {
// 根据当前的筛选条件加载指定页的数据
const params = {
pageNum: pageNum,
pageSize: this.pageParams.pageSize,
energyTypeId: this.filterParams.energyTypeId,
meterCode: this.filterParams.meterCode || undefined
}
listMeter(params).then(response => {
// 合并新加载的数据到现有列表
const startIndex = (pageNum - 1) * this.pageParams.pageSize
this.allMeterList.splice(startIndex, this.pageParams.pageSize, ...response.rows)
this.totalMeters = response.total
})
},
selectMeter(meter) {
this.selectedMeter = meter
this.recordForm.meterId = meter.meterId
this.resetRecord()
this.loadRateInfo(meter)
this.loadLastReading(meter)
},
loadLastReading(meter) {
// 获取上次抄表记录
getLastReading(meter.meterId).then(response => {
if (response.data) {
// 如果有上次抄表记录,自动填充起始读数为上次的结束读数
this.recordForm.startReading = response.data.endReading
this.recordForm.startTime = response.data.endTime
} else {
// 首次抄表,需要用户手动填写起始读数
this.recordForm.startReading = 0
this.$message.info('首次抄表,请填写起始读数')
}
}).catch(() => {
// 出错时,默认为首次抄表
this.recordForm.startReading = 0
})
},
loadRateInfo(meter) {
if (!meter || !meter.energyTypeId) return
listEnergyRate({ energyTypeId: meter.energyTypeId, pageNum: 1, pageSize: 1 }).then(response => {
if (response.rows && response.rows.length > 0) {
const rate = response.rows[0]
this.rateInfo = {
energyRateId: rate.energyRateId,
rate: rate.rate,
currency: rate.currency === 0 ? '元' : (rate.currency === 1 ? '$' : '€'),
effectiveDate: rate.effectiveDate,
expiryDate: rate.expiryDate,
usePeakValley: rate.usePeakValley,
useTieredPricing: rate.useTieredPricing,
tiers: rate.tiers || [], // 梯度列表
periods: rate.periods || [], // 峰谷时段列表
monthlyConsumption: 0, // 本月累计消耗量
monthlyEstimatedCost: 0 // 本月预估成本
}
// 加载本月的抄表记录
this.loadMonthlyConsumption(meter.meterId)
} else {
this.rateInfo = null
this.$message.warning('未找到该能源类型的费率信息')
}
}).catch(() => {
this.rateInfo = null
this.$message.error('获取费率信息失败')
})
},
loadMonthlyConsumption(meterId) {
// 调用后端接口获取本月累计消耗量
getMonthlyConsumption(meterId).then(response => {
if (response.data !== null && response.data !== undefined) {
const totalConsumption = parseFloat(response.data) || 0
this.rateInfo.monthlyConsumption = totalConsumption.toFixed(2)
// 计算本月预估成本
this.rateInfo.monthlyEstimatedCost = this.calculateCostByConsumption(totalConsumption, this.rateInfo)
console.log('本月累计消耗量:', this.rateInfo.monthlyConsumption, '本月预估成本:', this.rateInfo.monthlyEstimatedCost)
}
}).catch(() => {
this.rateInfo.monthlyConsumption = 0
this.rateInfo.monthlyEstimatedCost = 0
console.error('获取本月累计消耗量失败')
})
},
calculateCostByConsumption(consumption, rateInfo) {
if (!rateInfo) return 0
let cost = 0
// 固定费率模式
if (parseInt(rateInfo.usePeakValley) === 0 && parseInt(rateInfo.useTieredPricing) === 0) {
cost = consumption * parseFloat(rateInfo.rate)
}
// 峰谷模式 - 无法准确计算(需要知道各时段的消耗量)
else if (parseInt(rateInfo.usePeakValley) === 1 && parseInt(rateInfo.useTieredPricing) === 0) {
// 峰谷模式无法按实际计算,因为不知道各时段的消耗分布
// 这里返回0前端不显示预估成本
cost = 0
}
// 梯度模式 - 按实际梯度计算
else if (parseInt(rateInfo.usePeakValley) === 0 && parseInt(rateInfo.useTieredPricing) === 1) {
cost = this.calculateTieredCost(consumption, rateInfo.tiers)
}
// 梯度+峰谷组合模式 - 无法准确计算(需要知道各时段的消耗量)
else if (parseInt(rateInfo.usePeakValley) === 1 && parseInt(rateInfo.useTieredPricing) === 1) {
// 组合模式无法按实际计算,因为不知道各时段的消耗分布
// 这里返回0前端不显示预估成本
cost = 0
}
return isNaN(cost) ? 0 : cost.toFixed(2)
},
formatDate(date) {
if (!date) return '-'
return new Date(date).toLocaleDateString()
},
calculateCost() {
if (this.recordForm.startReading >= 0 && this.recordForm.endReading >= 0) {
this.recordForm.consumption = this.recordForm.endReading - this.recordForm.startReading
if (this.rateInfo) {
// 计算本次抄表的预估成本
// 需要加上本月已有的消耗量,这样才能正确计算梯度
const monthlyConsumption = parseFloat(this.rateInfo.monthlyConsumption) || 0
const totalConsumption = monthlyConsumption + this.recordForm.consumption
console.log('calculateCost - monthlyConsumption:', monthlyConsumption, 'consumption:', this.recordForm.consumption, 'totalConsumption:', totalConsumption)
let cost = 0
// 固定费率模式
if (parseInt(this.rateInfo.usePeakValley) === 0 && parseInt(this.rateInfo.useTieredPricing) === 0) {
const rate = parseFloat(this.rateInfo.rate) || 0
cost = totalConsumption * rate
console.log('固定费率模式 - rate:', rate, 'cost:', cost)
}
// 梯度模式 - 按总消耗量计算
else if (parseInt(this.rateInfo.usePeakValley) === 0 && parseInt(this.rateInfo.useTieredPricing) === 1) {
cost = this.calculateTieredCost(totalConsumption, this.rateInfo.tiers)
console.log('梯度模式 - totalConsumption:', totalConsumption, 'cost:', cost)
}
// 其他模式暂不计算
else {
cost = 0
}
this.recordForm.estimatedCost = isNaN(cost) ? 0 : cost.toFixed(2)
console.log('最终estimatedCost:', this.recordForm.estimatedCost)
}
}
},
calculateTieredCost(consumption, tiers) {
if (!tiers || tiers.length === 0) {
console.log('calculateTieredCost - 无梯度数据')
return 0
}
let cost = 0
let remainingConsumption = consumption
// 按梯度等级排序
const sortedTiers = [...tiers].sort((a, b) => a.tierLevel - b.tierLevel)
console.log('calculateTieredCost - sortedTiers:', sortedTiers)
for (const tier of sortedTiers) {
if (remainingConsumption <= 0) break
const minUsage = parseFloat(tier.minUsage) || 0
const maxUsage = tier.maxUsage === null || tier.maxUsage === undefined ? null : parseFloat(tier.maxUsage)
const rate = parseFloat(tier.rate) || 0
let tierConsumption = 0
if (maxUsage === null) {
// 最后一个梯度,无上限
tierConsumption = remainingConsumption
} else {
// 计算该梯度的消耗量
const tierRange = maxUsage - minUsage
tierConsumption = Math.min(remainingConsumption, tierRange)
}
const tierCost = tierConsumption * rate
cost += tierCost
remainingConsumption -= tierConsumption
console.log(`梯度${tier.tierLevel} - minUsage: ${minUsage}, maxUsage: ${maxUsage}, rate: ${rate}, tierConsumption: ${tierConsumption}, tierCost: ${tierCost}`)
}
console.log('calculateTieredCost - 总费用:', cost)
return cost
},
submitRecord() {
this.$refs.recordFormRef.validate((valid) => {
if (valid) {
this.recordForm.consumption = this.recordForm.endReading - this.recordForm.startReading
// 格式化日期为后端能接受的格式
const submitData = {
...this.recordForm,
startTime: this.formatDateForSubmit(this.recordForm.startTime),
endTime: this.formatDateForSubmit(this.recordForm.endTime)
}
addEnergyConsumption(submitData).then(() => {
this.$message.success('抄表记录提交成功')
this.resetRecord()
}).catch(() => {
this.$message.error('提交失败')
})
}
})
},
formatDateForSubmit(date) {
if (!date) return null
// 将日期转换为 yyyy-MM-dd HH:mm:ss 格式
const d = new Date(date)
const year = d.getFullYear()
const month = String(d.getMonth() + 1).padStart(2, '0')
const day = String(d.getDate()).padStart(2, '0')
const hours = String(d.getHours()).padStart(2, '0')
const minutes = String(d.getMinutes()).padStart(2, '0')
const seconds = String(d.getSeconds()).padStart(2, '0')
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
},
resetRecord() {
this.recordForm = {
meterId: this.selectedMeter ? this.selectedMeter.meterId : undefined,
startReading: 0,
endReading: 0, // 默认等于起始读数
consumption: 0,
estimatedCost: 0,
startTime: undefined,
endTime: undefined,
recordedBy: undefined,
remark: ''
}
this.$refs.recordFormRef && this.$refs.recordFormRef.resetFields()
},
// 当起始读数变化时,同步更新结束读数的最小值
onStartReadingChange() {
// 如果结束读数小于起始读数,自动设置为起始读数
if (this.recordForm.endReading < this.recordForm.startReading) {
this.recordForm.endReading = this.recordForm.startReading
}
this.calculateCost()
}
}
}
</script>
<style scoped lang="scss">
.energy-consumption-record {
padding: 20px;
background: #f5f7fa;
min-height: 100vh;
.filter-panel {
background: #ffffff;
padding: 16px;
border-radius: 4px;
margin-bottom: 20px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
::v-deep .el-form-item {
margin-bottom: 0;
}
}
.main-container {
.device-column {
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 300px;
color: #909399;
.empty-icon {
font-size: 48px;
margin-bottom: 12px;
}
.empty-text {
font-size: 14px;
}
}
.device-list-wrapper {
display: flex;
flex-direction: column;
height: 100%;
.device-list {
flex: 1;
overflow-y: auto;
padding-right: 8px;
margin-bottom: 12px;
&::-webkit-scrollbar {
width: 6px;
}
&::-webkit-scrollbar-track {
background: transparent;
}
&::-webkit-scrollbar-thumb {
background: #d9d9d9;
border-radius: 3px;
&:hover {
background: #bfbfbf;
}
}
}
.device-pagination {
text-align: center;
padding-top: 12px;
border-top: 1px solid #e8e8e8;
::v-deep .el-pagination {
display: flex;
justify-content: center;
}
}
}
.device-list .device-card {
background: #ffffff;
border: 1px solid #e8e8e8;
border-radius: 6px;
padding: 10px;
cursor: pointer;
transition: all 0.3s ease;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.04);
height: 100%;
display: flex;
flex-direction: column;
&:hover {
border-color: #409eff;
box-shadow: 0 4px 12px rgba(64, 158, 255, 0.15);
transform: translateY(-2px);
}
&.active {
background: #e6f7ff;
border-color: #409eff;
box-shadow: 0 4px 12px rgba(64, 158, 255, 0.25);
}
.card-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 6px;
gap: 4px;
.meter-code {
font-size: 13px;
font-weight: 600;
color: #303133;
flex: 1;
word-break: break-word;
}
::v-deep .el-tag {
flex-shrink: 0;
}
}
.card-body {
flex: 1;
.info-row {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 11px;
margin-bottom: 4px;
.label {
color: #909399;
min-width: 40px;
flex-shrink: 0;
}
.value {
color: #303133;
flex: 1;
text-align: right;
word-break: break-word;
}
&:last-child {
margin-bottom: 0;
}
}
}
}
}
.form-column {
.record-container {
display: flex;
flex-direction: column;
gap: 16px;
max-height: 70vh;
overflow-y: auto;
.rate-info-card,
.record-form-card {
background: #ffffff;
border-radius: 6px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
::v-deep .el-card__header {
background: #f9f9f9;
border-bottom: 1px solid #e8e8e8;
padding: 16px;
.title {
font-size: 16px;
font-weight: 600;
color: #303133;
}
}
::v-deep .el-card__body {
padding: 16px;
}
}
.rate-info-card {
.rate-content {
.rate-item {
display: flex;
align-items: center;
margin-bottom: 12px;
font-size: 14px;
.label {
color: #606266;
min-width: 80px;
font-weight: 500;
}
.rate-value {
font-size: 20px;
font-weight: bold;
color: #f56c6c;
margin: 0 4px;
}
.unit {
color: #909399;
}
.value {
color: #303133;
flex: 1;
}
&:last-child {
margin-bottom: 0;
}
}
}
}
.record-form-card {
::v-deep .el-form-item {
margin-bottom: 16px;
&:last-child {
margin-bottom: 0;
}
}
::v-deep .el-button {
margin-right: 10px;
}
}
.cost-display {
display: flex;
align-items: baseline;
gap: 8px;
padding: 12px;
background: #fef0f0;
border-radius: 4px;
.cost-value {
font-size: 24px;
font-weight: bold;
color: #f56c6c;
}
.cost-unit {
font-size: 14px;
color: #606266;
}
}
}
.empty-placeholder {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 400px;
color: #909399;
.empty-icon {
font-size: 64px;
margin-bottom: 16px;
}
.empty-text {
font-size: 16px;
}
.helper-text {
display: block;
font-size: 12px;
color: #909399;
margin-top: 4px;
}
}
}
}
}
</style>

View File

@@ -0,0 +1,257 @@
<template>
<div class="location-tree-node">
<!-- 节点头部 -->
<div class="node-header" :class="{ expanded: isExpanded }">
<div class="node-title">
<span
v-if="node.children && node.children.length > 0"
class="expand-icon"
@click="toggleExpand"
>
{{ isExpanded ? '▼' : '▶' }}
</span>
<span v-else class="expand-icon-placeholder" />
<span class="location-name">{{ node.name }}</span>
<el-tag type="info" size="small">{{ getDeviceCount(node.locationId) }}个设备</el-tag>
</div>
<div class="node-actions">
<el-button type="text" size="small" @click="$emit('edit', node)">编辑</el-button>
<el-button type="text" size="small" @click="$emit('add-device', node)">添加设备</el-button>
</div>
</div>
<!-- 设备列表 -->
<div v-if="isExpanded" class="device-list">
<div
v-for="device in getLocationDevices(node.locationId)"
:key="device.meterId"
class="device-item"
@click="$emit('device-click', device)"
>
<div class="device-info">
<span class="device-code">{{ device.meterCode }}</span>
<span class="device-energy">{{ getEnergyName(device.energyTypeId) }}</span>
<el-tag
:type="getStatusType(device.status)"
size="small"
>
{{ getStatusLabel(device.status) }}
</el-tag>
</div>
<div class="device-location">
{{ device.model }} / {{ device.manufacturer }}
</div>
</div>
<div v-if="getLocationDevices(node.locationId).length === 0" class="no-devices">
暂无设备
</div>
</div>
<!-- 子节点 -->
<div v-if="isExpanded && node.children && node.children.length > 0" class="children-nodes">
<location-tree-node
v-for="child in node.children"
:key="child.locationId"
:node="child"
:meter-list="meterList"
:energy-type-list="energyTypeList"
@edit="$emit('edit', $event)"
@add-device="$emit('add-device', $event)"
@device-click="$emit('device-click', $event)"
/>
</div>
</div>
</template>
<script>
export default {
name: 'LocationTreeNode',
props: {
node: {
type: Object,
required: true
},
meterList: {
type: Array,
default: () => []
},
energyTypeList: {
type: Array,
default: () => []
}
},
emits: ['edit', 'add-device', 'device-click'],
data() {
return {
isExpanded: false
};
},
methods: {
toggleExpand() {
this.isExpanded = !this.isExpanded;
},
getLocationDevices(locationId) {
return this.meterList.filter(m => m.locationId === locationId);
},
getDeviceCount(locationId) {
return this.getLocationDevices(locationId).length;
},
getEnergyName(energyTypeId) {
const energy = this.energyTypeList.find(item => item.energyTypeId === energyTypeId);
return energy ? energy.name : '-';
},
getStatusLabel(status) {
const statusMap = {
0: '在用',
1: '停用',
2: '维护'
};
return statusMap[status] || '未知';
},
getStatusType(status) {
const typeMap = {
0: 'success',
1: 'info',
2: 'warning'
};
return typeMap[status] || 'info';
}
}
};
</script>
<style lang="scss" scoped>
.location-tree-node {
margin-bottom: 8px;
border: 1px solid #ebeef5;
border-radius: 4px;
overflow: hidden;
.node-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 16px;
background: #f5f7fa;
cursor: pointer;
transition: background 0.3s ease;
&:hover {
background: #e8f4f8;
}
&.expanded {
background: #e8f4f8;
}
.node-title {
display: flex;
align-items: center;
gap: 8px;
flex: 1;
.expand-icon {
width: 20px;
height: 20px;
cursor: pointer;
transition: transform 0.3s ease;
color: #409eff;
&:hover {
transform: scale(1.2);
}
}
.expand-icon-placeholder {
width: 20px;
height: 20px;
display: inline-block;
}
.location-name {
font-weight: 600;
color: #303133;
font-size: 14px;
}
}
.node-actions {
display: flex;
gap: 8px;
}
}
.device-list {
padding: 12px 16px;
background: #fafafa;
border-top: 1px solid #ebeef5;
.device-item {
padding: 10px 12px;
background: #ffffff;
border-radius: 4px;
margin-bottom: 8px;
cursor: pointer;
transition: all 0.3s ease;
border: 1px solid #e8e8e8;
&:last-child {
margin-bottom: 0;
}
&:hover {
border-color: #409eff;
box-shadow: 0 2px 8px rgba(64, 158, 255, 0.1);
transform: translateX(4px);
}
.device-info {
display: flex;
gap: 8px;
align-items: center;
margin-bottom: 4px;
.device-code {
font-weight: 600;
color: #303133;
font-size: 13px;
}
.device-energy {
color: #909399;
font-size: 12px;
}
}
.device-location {
font-size: 12px;
color: #909399;
padding-left: 8px;
}
}
.no-devices {
text-align: center;
color: #909399;
padding: 20px 0;
font-size: 12px;
}
}
.children-nodes {
padding: 12px 16px;
background: #fafafa;
border-top: 1px solid #ebeef5;
margin-left: 16px;
border-left: 2px solid #d9d9d9;
}
}
</style>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,827 @@
<template>
<div class="meter-manage-page">
<!-- 统计数据展示 -->
<div class="statistics-panel">
<div
v-for="energyType in energyTypeList"
:key="energyType.energyTypeId"
class="stat-item"
>
<div class="stat-label">{{ energyType.name }}</div>
<div class="stat-value">{{ statistics[`energy_${energyType.energyTypeId}`] || 0 }}</div>
<div class="stat-sub">
停用 {{ statistics[`energy_${energyType.energyTypeId}_disabled`] || 0 }}
维护 {{ statistics[`energy_${energyType.energyTypeId}_maintenance`] || 0 }}
</div>
</div>
</div>
<!-- 顶部工具栏 -->
<div class="toolbar">
<el-button type="primary" icon="el-icon-plus" @click="handleAddMeter">新增设备</el-button>
<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>
<input
ref="importFile"
type="file"
accept=".xlsx,.xls"
style="display: none"
@change="onFileSelected"
/>
</div>
<!-- 搜索过滤区 -->
<el-collapse-transition>
<div class="search-panel" v-show="showSearch">
<el-form :model="queryParams" ref="queryForm" size="small" :inline="true" label-width="80px">
<el-form-item label="设备编号">
<el-input v-model="queryParams.meterCode" placeholder="请输入设备编号" clearable @keyup.enter.native="handleQuery" />
</el-form-item>
<el-form-item label="能源类型">
<el-select v-model="queryParams.energyTypeId" placeholder="全部" clearable @change="handleQuery">
<el-option v-for="item in energyTypeList" :key="item.energyTypeId" :label="item.name" :value="item.energyTypeId" />
</el-select>
</el-form-item>
<el-form-item label="型号">
<el-input v-model="queryParams.model" placeholder="设备型号" clearable @keyup.enter.native="handleQuery" />
</el-form-item>
<el-form-item label="制造商">
<el-input v-model="queryParams.manufacturer" placeholder="请输入制造商" clearable @keyup.enter.native="handleQuery" />
</el-form-item>
<el-form-item>
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
<el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
</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">
<span class="label">绑定库区</span>
<el-tag
v-if="getBindingWarehouse(meter.meterId)"
type="success"
size="small"
>
{{ getBindingWarehouse(meter.meterId) }}
</el-tag>
<el-tag v-else type="info" size="small">未绑定</el-tag>
</div>
<div class="info-item status-selector">
<span class="label">状态</span>
<el-radio-group
v-model="meter.status"
size="small"
@input="handleStatusChange(meter)"
>
<el-radio :label="0">在用</el-radio>
<el-radio :label="1">停用</el-radio>
<el-radio :label="2">维护</el-radio>
</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>
</el-col>
</el-row>
</div>
<!-- 新增/编辑设备对话框 -->
<el-dialog title="设备信息" :visible.sync="meterDialogVisible" width="800px" @close="resetMeterForm">
<el-form :model="meterForm" :rules="meterRules" ref="meterFormRef" label-width="120px">
<!-- 能源类型选择 -->
<el-form-item label="能源类型" prop="energyTypeId">
<div class="energy-type-selector">
<div
v-for="item in energyTypeList"
:key="item.energyTypeId"
class="energy-card"
:class="{ active: meterForm.energyTypeId === item.energyTypeId }"
@click="selectEnergyType(item)"
>
<svg-icon :icon-class="getEnergyIcon(item.name)" class="energy-icon"></svg-icon>
<div class="energy-name">{{ item.name }}</div>
</div>
</div>
</el-form-item>
<!-- 设备编号 -->
<el-form-item label="设备编号" prop="meterCode">
<el-input
v-model="meterForm.meterCode"
placeholder="请输入设备编号(如 W-001"
:disabled="isEditMode"
maxlength="50"
/>
<div class="form-hint">选择能源类型后前缀会自动填入</div>
</el-form-item>
<!-- 基本信息 -->
<el-form-item label="型号" prop="model">
<el-input v-model="meterForm.model" placeholder="请输入设备型号" />
</el-form-item>
<el-form-item label="制造商" prop="manufacturer">
<el-input v-model="meterForm.manufacturer" placeholder="请输入制造商" />
</el-form-item>
<el-form-item label="安装日期" prop="installDate">
<el-date-picker v-model="meterForm.installDate" type="date" placeholder="选择日期" value-format="yyyy-MM-dd" />
</el-form-item>
</el-form>
<span slot="footer" class="dialog-footer">
<el-button @click="meterDialogVisible = false"> </el-button>
<el-button type="primary" @click="submitMeterForm"> </el-button>
</span>
</el-dialog>
</div>
</template>
<script>
import { listMeter, getMeter, addMeter, updateMeter, delMeter, downloadMeterTemplate, importMeters } from "@/api/ems/meter";
import { listEnergyType } from "@/api/ems/energyType";
import { listWarehouse } from '@/api/wms/warehouse'
import { fetchEnergyLinkMatrix, addEnergyLink } from '@/api/ems/energyLink'
export default {
name: "MeterManage",
data() {
return {
loading: false,
importLoading: false,
showSearch: false,
meterList: [],
energyTypeList: [],
warehouseList: [],
matrixData: [],
// 统计数据
statistics: {
waterMeters: 0,
electricMeters: 0,
gasMeters: 0,
waterDisabled: 0,
electricDisabled: 0,
gasDisabled: 0,
waterMaintenance: 0,
electricMaintenance: 0,
gasMaintenance: 0
},
queryParams: {
pageNum: 1,
pageSize: 9999,
meterCode: undefined,
energyTypeId: undefined,
model: undefined,
manufacturer: undefined,
installDate: undefined,
},
meterDialogVisible: false,
isEditMode: false,
meterForm: {},
meterRules: {
meterCode: [{ required: true, message: '设备编号不能为空', trigger: 'blur' }],
energyTypeId: [{ required: true, message: '能源类型不能为空', trigger: 'blur' }],
model: [{ required: true, message: '型号不能为空', trigger: 'blur' }],
manufacturer: [{ required: true, message: '制造商不能为空', trigger: 'blur' }],
}
};
},
created() {
Promise.all([
this.getList(),
this.getEnergyTypeList(),
this.loadWarehouseList(),
this.loadMatrix()
]).then(() => {
this.calculateStatistics();
});
},
methods: {
getList() {
this.loading = true;
return listMeter(this.queryParams).then(response => {
this.meterList = response.rows;
this.calculateStatistics();
}).finally(() => {
this.loading = false;
});
},
getEnergyTypeList() {
return listEnergyType().then(response => {
this.energyTypeList = response.rows;
});
},
loadWarehouseList() {
return listWarehouse({ pageNum: 1, pageSize: 9999 }).then(response => {
this.warehouseList = response.rows;
});
},
loadMatrix() {
fetchEnergyLinkMatrix().then(res => {
this.matrixData = res.data || [];
});
},
getEnergyName(energyTypeId) {
const item = this.energyTypeList.find(e => e.energyTypeId === energyTypeId);
return item ? item.name : '-';
},
getStatusText(status) {
const statusMap = {
0: '在用',
1: '停用',
2: '维护'
};
return statusMap[status] || '未知';
},
getStatusType(status) {
const typeMap = {
0: 'success',
1: 'danger',
2: 'warning'
};
return typeMap[status] || 'info';
},
getBindingWarehouse(meterId) {
for (let warehouse of this.matrixData) {
for (let link of (warehouse.links || [])) {
if (link.meterId === meterId) {
return warehouse.warehouseName;
}
}
}
return null;
},
calculateStatistics() {
// 根据能源类型表动态生成统计数据结构
this.statistics = {};
// 为每个能源类型初始化统计数据
this.energyTypeList.forEach(energyType => {
const key = energyType.energyTypeId;
this.statistics[`energy_${key}`] = 0;
this.statistics[`energy_${key}_disabled`] = 0;
this.statistics[`energy_${key}_maintenance`] = 0;
});
// 统计设备数量
this.meterList.forEach(meter => {
const key = meter.energyTypeId;
// 转换 status 为数字,处理字符串和 null 的情况
const status = meter.status !== null && meter.status !== undefined ? parseInt(meter.status) : 0;
if (this.statistics[`energy_${key}`] !== undefined) {
this.statistics[`energy_${key}`]++;
if (status === 1) this.statistics[`energy_${key}_disabled`]++;
if (status === 2) this.statistics[`energy_${key}_maintenance`]++;
}
});
},
handleQuery() {
this.queryParams.pageNum = 1;
this.getList();
},
resetQuery() {
this.$refs.queryForm.resetFields();
this.queryParams.pageNum = 1;
this.getList();
},
handleAddMeter() {
this.isEditMode = false;
this.meterForm = {
meterCode: '',
energyTypeId: null,
model: '',
manufacturer: '',
installDate: null
};
this.meterDialogVisible = true;
},
handleEditMeter(meter) {
this.isEditMode = true;
this.meterForm = JSON.parse(JSON.stringify(meter));
this.meterDialogVisible = true;
},
selectEnergyType(energyType) {
this.meterForm.energyTypeId = energyType.energyTypeId;
// 新增模式下,直接设置编号前缀
if (!this.isEditMode) {
const prefix = this.getEnergyPrefix(energyType.name);
this.meterForm.meterCode = prefix;
}
},
getEnergyPrefix(energyName) {
const prefixMap = {
'水': 'W-',
'电': 'E-',
'天然气': 'G-'
};
return prefixMap[energyName] || '';
},
getEnergyIcon(energyName) {
const iconMap = {
'水': 'shuibiao',
'电': 'dianbiao',
'天然气': 'qibiao'
};
return iconMap[energyName] || 'shuibiao';
},
selectWarehouse(warehouse) {
this.meterForm.warehouseId = warehouse ? warehouse.warehouseId : null;
if (!warehouse) {
this.meterForm.allocationMode = null;
this.meterForm.weightRatio = null;
}
},
submitMeterForm() {
this.$refs.meterFormRef.validate((valid) => {
if (valid) {
const submitFn = this.isEditMode ? updateMeter : addMeter;
submitFn(this.meterForm).then(() => {
this.$message.success(this.isEditMode ? '编辑成功' : '新增成功');
this.meterDialogVisible = false;
this.getList();
this.loadMatrix();
// 如果绑定了库区,自动创建绑定关系
if (this.meterForm.warehouseId && !this.isEditMode) {
const linkData = {
meterId: this.meterForm.meterId,
warehouseId: this.meterForm.warehouseId,
energyTypeId: this.meterForm.energyTypeId,
allocationMode: this.meterForm.allocationMode || 'average',
weightRatio: this.meterForm.weightRatio || 1,
requirePendingAction: 0
};
addEnergyLink(linkData).then(() => {
this.$message.success('设备绑定成功');
this.loadMatrix();
});
}
}).catch(() => {
this.$message.error(this.isEditMode ? '编辑失败' : '新增失败');
});
}
});
},
resetMeterForm() {
this.$refs.meterFormRef.resetFields();
this.meterForm = {};
this.isEditMode = false;
},
handleDeleteMeter(meter) {
this.$confirm('确定删除该设备吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
delMeter(meter.meterId).then(() => {
this.$message.success('删除成功');
this.getList();
this.loadMatrix();
}).catch(() => {
this.$message.error('删除失败');
});
}).catch(() => {});
},
handleStatusChange(meter) {
// 确保状态值是数字
const status = typeof meter.status === 'string' ? parseInt(meter.status) : meter.status;
// 更新设备状态
updateMeter({
meterId: meter.meterId,
status: status
}).then(() => {
this.$message.success('状态更新成功');
this.calculateStatistics();
}).catch(() => {
this.$message.error('状态更新失败');
// 恢复原状态
this.getList();
});
},
downloadTemplate() {
this.$message.success('正在下载模板...');
downloadMeterTemplate().then(res => {
const url = window.URL.createObjectURL(res);
const link = document.createElement('a');
link.href = url;
link.download = '设备导入模板.xlsx';
link.click();
window.URL.revokeObjectURL(url);
this.$message.success('模板下载成功');
}).catch(() => {
this.$message.error('模板下载失败');
});
},
handleImport() {
this.$refs.importFile.click();
},
onFileSelected(event) {
const file = event.target.files[0];
if (!file) return;
this.importLoading = true;
const loadingInstance = this.$loading({
lock: true,
text: '正在导入设备...',
spinner: 'el-icon-loading',
background: 'rgba(0, 0, 0, 0.7)'
});
importMeters(file).then(res => {
loadingInstance.close();
this.$message.success('设备导入成功');
this.getList();
this.loadMatrix();
this.calculateStatistics();
// 重置文件输入
this.$refs.importFile.value = '';
}).catch(err => {
loadingInstance.close();
this.$message.error(err.msg || '设备导入失败');
// 重置文件输入
this.$refs.importFile.value = '';
}).finally(() => {
this.importLoading = false;
});
}
}
};
</script>
<style lang="scss" scoped>
.meter-manage-page {
padding: 20px;
background: #f6f7fb;
min-height: calc(100vh - 100px);
.statistics-panel {
display: flex;
gap: 20px;
margin-bottom: 20px;
padding: 20px;
background: #fafafa;
border-radius: 4px;
border: 1px solid #ebeef5;
.stat-item {
flex: 1;
text-align: center;
.stat-label {
font-size: 14px;
color: #606266;
margin-bottom: 10px;
}
.stat-value {
font-size: 32px;
font-weight: bold;
color: #409eff;
}
.stat-sub {
font-size: 12px;
color: #909399;
margin-top: 6px;
}
}
}
.toolbar {
display: flex;
gap: 10px;
margin-bottom: 20px;
}
.search-panel {
background: #ffffff;
padding: 16px;
border-radius: 10px;
margin-bottom: 20px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
}
.devices-container {
padding: 0;
background: transparent;
}
.empty-state {
text-align: center;
padding: 60px 20px;
background: #ffffff;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
.empty-icon {
font-size: 48px;
margin-bottom: 16px;
}
.empty-text {
font-size: 16px;
color: #909399;
}
}
.device-cards {
.device-card {
background: #ffffff;
border-radius: 8px;
padding: 16px;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.04);
border: 1px solid #e8e8e8;
transition: all 0.3s ease;
display: flex;
flex-direction: column;
height: 100%;
&:hover {
transform: translateY(-4px);
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.1);
border-color: #409eff;
}
.card-header {
margin-bottom: 12px;
padding-bottom: 12px;
border-bottom: 1px solid #f0f0f0;
display: flex;
justify-content: space-between;
align-items: flex-start;
.header-left {
flex: 1;
}
.meter-code {
font-size: 16px;
font-weight: 600;
color: #303133;
margin-bottom: 4px;
}
.energy-type {
font-size: 12px;
color: #909399;
}
}
.card-body {
flex: 1;
margin-bottom: 12px;
.info-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 6px 0;
font-size: 13px;
.label {
color: #909399;
min-width: 60px;
}
.value {
color: #303133;
font-weight: 500;
text-align: right;
flex: 1;
margin-left: 8px;
word-break: break-word;
}
&.status-selector {
align-items: flex-start;
padding: 8px 0;
flex-wrap: wrap;
.label {
width: 100%;
margin-bottom: 4px;
}
::v-deep .el-radio-group {
display: flex;
flex-wrap: wrap;
gap: 12px;
width: 100%;
.el-radio {
margin-right: 0;
}
}
}
}
}
.card-footer {
display: flex;
gap: 8px;
padding-top: 12px;
border-top: 1px solid #f0f0f0;
}
}
}
}
// 卡片按钮样式
::v-deep .device-card .card-footer .el-button {
flex: 1;
padding: 6px 0 !important;
font-size: 12px !important;
}
// 能源类型卡片选择
.energy-type-selector {
display: flex;
gap: 16px;
flex-wrap: wrap;
}
.energy-card {
flex: 0 0 calc(33.333% - 11px);
min-width: 100px;
padding: 12px;
border: 2px solid #e8e8e8;
border-radius: 6px;
text-align: center;
cursor: pointer;
transition: all 0.3s ease;
background: #ffffff;
&:hover {
border-color: #409eff;
box-shadow: 0 2px 12px rgba(64, 158, 255, 0.15);
}
&.active {
border-color: #409eff;
background: #f0f9ff;
box-shadow: 0 2px 12px rgba(64, 158, 255, 0.25);
}
.energy-icon {
width: 24px;
height: 24px;
margin: 0 auto 6px;
}
.energy-name {
font-size: 12px;
color: #303133;
font-weight: 500;
}
}
// 库区卡片选择
.warehouse-selector {
display: flex;
gap: 12px;
flex-wrap: wrap;
}
.warehouse-card {
flex: 0 0 calc(25% - 9px);
min-width: 140px;
padding: 16px;
border: 2px solid #e8e8e8;
border-radius: 8px;
cursor: pointer;
transition: all 0.3s ease;
background: #ffffff;
text-align: center;
&:hover {
border-color: #409eff;
box-shadow: 0 2px 12px rgba(64, 158, 255, 0.15);
}
&.active {
border-color: #409eff;
background: #f0f9ff;
box-shadow: 0 2px 12px rgba(64, 158, 255, 0.25);
}
&.empty {
display: flex;
align-items: center;
justify-content: center;
color: #909399;
}
.warehouse-name {
font-size: 14px;
color: #303133;
font-weight: 500;
margin-bottom: 4px;
}
.warehouse-code {
font-size: 12px;
color: #909399;
}
}
// 表单提示
.form-hint {
font-size: 12px;
color: #909399;
margin-top: 4px;
}
// 对话框样式
::v-deep .el-dialog {
.el-dialog__body {
padding: 30px;
}
.el-form-item {
margin-bottom: 24px;
}
.el-form-item__label {
font-weight: 500;
color: #303133;
}
}
</style>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,311 @@
<template>
<div class="allocation-task-page">
<!-- 查询条件 -->
<el-card class="search-card">
<el-form :model="queryParams" label-width="120px" size="small">
<el-row :gutter="20">
<el-col :xs="24" :sm="12" :md="6">
<el-form-item label="任务日期:">
<el-date-picker v-model="queryParams.taskDate" type="date" placeholder="选择任务日期" value-format="yyyy-MM-dd" @change="handleQuery"></el-date-picker>
</el-form-item>
</el-col>
<el-col :xs="24" :sm="12" :md="6">
<el-form-item label="能源类型:">
<el-select v-model="queryParams.energyTypeId" placeholder="请选择能源类型" clearable @change="handleQuery">
<el-option v-for="item in energyTypeList" :key="item.id" :label="item.name" :value="item.id"></el-option>
</el-select>
</el-form-item>
</el-col>
<el-col :xs="24" :sm="12" :md="6">
<el-form-item label="任务状态:">
<el-select v-model="queryParams.status" placeholder="请选择任务状态" clearable @change="handleQuery">
<el-option label="待执行" :value="0"></el-option>
<el-option label="执行中" :value="1"></el-option>
<el-option label="已完成" :value="2"></el-option>
<el-option label="失败" :value="3"></el-option>
</el-select>
</el-form-item>
</el-col>
<el-col :xs="24" :sm="12" :md="6">
<el-button type="primary" icon="el-icon-search" size="small" @click="handleQuery">查询</el-button>
<el-button icon="el-icon-refresh" size="small" @click="resetQuery">重置</el-button>
<el-button type="success" icon="el-icon-plus" size="small" @click="openAddDialog">新增任务</el-button>
</el-col>
</el-row>
</el-form>
</el-card>
<!-- 任务列表 -->
<el-card class="list-card" v-loading="loading">
<div slot="header" class="clearfix">
<span class="card-title">能源分摊任务列表</span>
</div>
<el-table :data="taskList" stripe border>
<el-table-column prop="taskId" label="任务ID" width="100"></el-table-column>
<el-table-column prop="taskDate" label="任务日期" width="120"></el-table-column>
<el-table-column prop="energyTypeName" label="能源类型" width="100"></el-table-column>
<el-table-column prop="allocationScope" label="分摊范围" width="100"></el-table-column>
<el-table-column prop="status" label="任务状态" width="100">
<template slot-scope="scope">
<el-tag v-if="scope.row.status === 0" type="info">待执行</el-tag>
<el-tag v-else-if="scope.row.status === 1" type="warning">执行中</el-tag>
<el-tag v-else-if="scope.row.status === 2" type="success">已完成</el-tag>
<el-tag v-else type="danger">失败</el-tag>
</template>
</el-table-column>
<el-table-column prop="executeMode" label="执行方式" width="100"></el-table-column>
<el-table-column prop="totalConsumption" label="总消耗量" width="120">
<template slot-scope="scope">
{{ formatNumber(scope.row.totalConsumption, 2) }}
</template>
</el-table-column>
<el-table-column prop="totalCost" label="总成本" width="120">
<template slot-scope="scope">
¥ {{ formatNumber(scope.row.totalCost, 2) }}
</template>
</el-table-column>
<el-table-column prop="createTime" label="创建时间" width="180"></el-table-column>
<el-table-column label="操作" width="200" fixed="right">
<template slot-scope="scope">
<el-button
v-if="scope.row.status === 0 || scope.row.status === 3"
type="primary"
size="mini"
@click="runTask(scope.row)">
执行
</el-button>
<el-button
v-if="scope.row.status === 2"
type="warning"
size="mini"
@click="rerunTask(scope.row)">
重新执行
</el-button>
<el-button type="info" size="mini" @click="viewDetail(scope.row)">详情</el-button>
<el-button type="danger" size="mini" @click="deleteTask(scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<el-pagination
:current-page="queryParams.pageNum"
:page-sizes="[10, 20, 50, 100]"
:page-size="queryParams.pageSize"
layout="total, sizes, prev, pager, next, jumper"
:total="total"
@size-change="handlePageSizeChange"
@current-change="handlePageChange"
style="margin-top: 20px; text-align: right;"
></el-pagination>
</el-card>
<!-- 新增任务对话框 -->
<el-dialog title="新增分摊任务" :visible.sync="addDialogVisible" width="500px" @close="closeAddDialog">
<el-form :model="addForm" label-width="120px" size="small" ref="addFormRef">
<el-form-item label="任务日期:" prop="taskDate">
<el-date-picker v-model="addForm.taskDate" type="date" placeholder="选择任务日期" value-format="yyyy-MM-dd"></el-date-picker>
</el-form-item>
<el-form-item label="能源类型:" prop="energyTypeId">
<el-select v-model="addForm.energyTypeId" placeholder="请选择能源类型(留空则全部)">
<el-option label="全部能源类型" :value="null"></el-option>
<el-option v-for="item in energyTypeList" :key="item.id" :label="item.name" :value="item.id"></el-option>
</el-select>
</el-form-item>
<el-form-item label="分摊范围:" prop="allocationScope">
<el-select v-model="addForm.allocationScope" placeholder="请选择分摊范围">
<el-option label="按库区" value="warehouse"></el-option>
<el-option label="按仪表" value="meter"></el-option>
</el-select>
</el-form-item>
</el-form>
<span slot="footer" class="dialog-footer">
<el-button @click="closeAddDialog">取消</el-button>
<el-button type="primary" @click="submitAddTask">确定</el-button>
</span>
</el-dialog>
<!-- 详情对话框 -->
<el-dialog title="任务详情" :visible.sync="detailDialogVisible" width="600px" @close="closeDetailDialog">
<el-descriptions :column="2" border v-if="selectedTask">
<el-descriptions-item label="任务ID">{{ selectedTask.taskId }}</el-descriptions-item>
<el-descriptions-item label="任务日期">{{ selectedTask.taskDate }}</el-descriptions-item>
<el-descriptions-item label="能源类型">{{ selectedTask.energyTypeName }}</el-descriptions-item>
<el-descriptions-item label="分摊范围">{{ selectedTask.allocationScope }}</el-descriptions-item>
<el-descriptions-item label="任务状态">
<el-tag v-if="selectedTask.status === 0" type="info">待执行</el-tag>
<el-tag v-else-if="selectedTask.status === 1" type="warning">执行中</el-tag>
<el-tag v-else-if="selectedTask.status === 2" type="success">已完成</el-tag>
<el-tag v-else type="danger">失败</el-tag>
</el-descriptions-item>
<el-descriptions-item label="执行方式">{{ selectedTask.executeMode }}</el-descriptions-item>
<el-descriptions-item label="总消耗量">{{ formatNumber(selectedTask.totalConsumption, 2) }}</el-descriptions-item>
<el-descriptions-item label="总成本">¥ {{ formatNumber(selectedTask.totalCost, 2) }}</el-descriptions-item>
<el-descriptions-item label="创建时间" :span="2">{{ selectedTask.createTime }}</el-descriptions-item>
<el-descriptions-item label="错误信息" :span="2" v-if="selectedTask.message">
<el-alert :title="selectedTask.message" type="error" :closable="false"></el-alert>
</el-descriptions-item>
</el-descriptions>
</el-dialog>
</div>
</template>
<script>
import { listEnergyTask } from '@/api/ems/energyAllocation'
export default {
name: 'AllocationTask',
data() {
return {
loading: false,
total: 0,
queryParams: {
taskDate: null,
energyTypeId: null,
status: null,
pageNum: 1,
pageSize: 20
},
taskList: [],
energyTypeList: [
{ id: 1, name: '电' },
{ id: 2, name: '水' },
{ id: 3, name: '气' }
],
addDialogVisible: false,
addForm: {
taskDate: null,
energyTypeId: null,
allocationScope: 'warehouse'
},
detailDialogVisible: false,
selectedTask: null
};
},
mounted() {
this.handleQuery();
},
methods: {
handleQuery() {
this.loading = true;
listEnergyTask(this.queryParams).then(response => {
this.taskList = response.rows || [];
this.total = response.total || 0;
}).catch(() => {
this.$message.error('加载任务列表失败');
}).finally(() => {
this.loading = false;
});
},
resetQuery() {
this.queryParams = {
taskDate: null,
energyTypeId: null,
status: null,
pageNum: 1,
pageSize: 20
};
this.handleQuery();
},
handlePageChange(page) {
this.queryParams.pageNum = page;
this.handleQuery();
},
handlePageSizeChange(size) {
this.queryParams.pageSize = size;
this.handleQuery();
},
openAddDialog() {
this.addForm = {
taskDate: null,
energyTypeId: null,
allocationScope: 'warehouse'
};
this.addDialogVisible = true;
},
closeAddDialog() {
this.addDialogVisible = false;
this.$refs.addFormRef && this.$refs.addFormRef.clearValidate();
},
submitAddTask() {
if (!this.addForm.taskDate) {
this.$message.error('请选择任务日期');
return;
}
// TODO: 调用后端API创建任务
this.$message.success('任务创建成功');
this.closeAddDialog();
this.handleQuery();
},
runTask(row) {
this.$confirm('确定要执行该任务吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
// TODO: 调用后端API执行任务
this.$message.success('任务已提交执行');
this.handleQuery();
}).catch(() => {});
},
rerunTask(row) {
this.$confirm('确定要重新执行该任务吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
// TODO: 调用后端API重新执行任务
this.$message.success('任务已重新提交执行');
this.handleQuery();
}).catch(() => {});
},
viewDetail(row) {
this.selectedTask = row;
this.detailDialogVisible = true;
},
closeDetailDialog() {
this.detailDialogVisible = false;
this.selectedTask = null;
},
deleteTask(row) {
this.$confirm('确定删除该任务吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
// TODO: 调用后端API删除任务
this.$message.success('删除成功');
this.handleQuery();
}).catch(() => {});
},
formatNumber(value, decimals = 2) {
if (value === null || value === undefined) return '0.00';
return parseFloat(value).toFixed(decimals);
}
}
};
</script>
<style scoped>
.allocation-task-page {
padding: 20px;
}
.search-card {
margin-bottom: 20px;
}
.list-card {
margin-bottom: 20px;
}
.card-title {
font-size: 16px;
font-weight: bold;
}
.dialog-footer {
text-align: right;
}
</style>