feat: add energy monitoring frontend pages
This commit is contained in:
parent
913d6400e4
commit
5500238be3
|
|
@ -0,0 +1,93 @@
|
|||
import request from '@/utils/request'
|
||||
|
||||
export interface EnergyMeter {
|
||||
id?: string
|
||||
meterCode: string
|
||||
meterName: string
|
||||
energyType: string
|
||||
installationLocation?: string
|
||||
ratedCapacity?: number
|
||||
unitPrice?: number
|
||||
status?: string
|
||||
}
|
||||
|
||||
export interface EnergyConsumption {
|
||||
id?: string
|
||||
meterId: string
|
||||
consumptionDate: string
|
||||
previousReading: number
|
||||
currentReading: number
|
||||
consumption: number
|
||||
amount?: number
|
||||
recordMethod?: string
|
||||
}
|
||||
|
||||
export function getEnergyMeters(projectId: string, energyType?: string) {
|
||||
return request({
|
||||
url: '/api/v1/ops/energy-meters',
|
||||
method: 'get',
|
||||
params: { projectId, energyType }
|
||||
})
|
||||
}
|
||||
|
||||
export function getEnergyMeter(id: string) {
|
||||
return request({
|
||||
url: `/api/v1/ops/energy-meters/${id}`,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
export function createEnergyMeter(data: EnergyMeter) {
|
||||
return request({
|
||||
url: '/api/v1/ops/energy-meters',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
export function updateEnergyMeter(id: string, data: EnergyMeter) {
|
||||
return request({
|
||||
url: `/api/v1/ops/energy-meters/${id}`,
|
||||
method: 'put',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
export function deleteEnergyMeter(id: string) {
|
||||
return request({
|
||||
url: `/api/v1/ops/energy-meters/${id}`,
|
||||
method: 'delete'
|
||||
})
|
||||
}
|
||||
|
||||
export function recordEnergyConsumption(data: { meterId: string; currentReading: number; recordedBy?: string }) {
|
||||
return request({
|
||||
url: '/api/v1/ops/energy-consumption',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
export function getEnergyConsumption(meterId: string, startDate?: string, endDate?: string) {
|
||||
return request({
|
||||
url: `/api/v1/ops/energy-consumption/${meterId}`,
|
||||
method: 'get',
|
||||
params: { startDate, endDate }
|
||||
})
|
||||
}
|
||||
|
||||
export function getConsumptionByType(projectId: string, month: string) {
|
||||
return request({
|
||||
url: '/api/v1/ops/energy-statistics/by-type',
|
||||
method: 'get',
|
||||
params: { projectId, month }
|
||||
})
|
||||
}
|
||||
|
||||
export function getUnitConsumption(projectId: string, month: string) {
|
||||
return request({
|
||||
url: '/api/v1/ops/energy-statistics/unit-consumption',
|
||||
method: 'get',
|
||||
params: { projectId, month }
|
||||
})
|
||||
}
|
||||
|
|
@ -92,6 +92,24 @@ const router = createRouter({
|
|||
name: 'MaintenanceTasks',
|
||||
component: () => import('@/views/maintenance/TaskList.vue'),
|
||||
meta: { title: '维保任务' }
|
||||
},
|
||||
{
|
||||
path: 'energy/meters',
|
||||
name: 'EnergyMeters',
|
||||
component: () => import('@/views/energy/MeterList.vue'),
|
||||
meta: { title: '计量点管理' }
|
||||
},
|
||||
{
|
||||
path: 'energy/consumption',
|
||||
name: 'EnergyConsumption',
|
||||
component: () => import('@/views/energy/ConsumptionRecord.vue'),
|
||||
meta: { title: '能耗录入' }
|
||||
},
|
||||
{
|
||||
path: 'energy/statistics',
|
||||
name: 'EnergyStatistics',
|
||||
component: () => import('@/views/energy/EnergyStatistics.vue'),
|
||||
meta: { title: '能耗统计' }
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import {
|
|||
TeamOutlined,
|
||||
AppstoreOutlined,
|
||||
BuildOutlined,
|
||||
HeatMapOutlined,
|
||||
LogoutOutlined,
|
||||
AuditOutlined,
|
||||
SettingOutlined,
|
||||
|
|
@ -55,6 +56,16 @@ const menuItems: MenuProps['items'] = [
|
|||
key: '/maintenance/tasks',
|
||||
icon: () => h(ToolOutlined),
|
||||
label: '维保任务'
|
||||
},
|
||||
{
|
||||
key: 'energy',
|
||||
icon: () => h(HeatMapOutlined),
|
||||
label: '能耗管理',
|
||||
children: [
|
||||
{ key: '/energy/meters', label: '计量点管理' },
|
||||
{ key: '/energy/consumption', label: '能耗录入' },
|
||||
{ key: '/energy/statistics', label: '能耗统计' }
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
|
|||
|
|
@ -0,0 +1,417 @@
|
|||
<script setup lang="ts">
|
||||
import { ref, reactive, computed, onMounted } from 'vue'
|
||||
import { Button, Select, Space, message, Card, Statistic, Row, Col, Table, DatePicker, InputNumber, Form } from 'ant-design-vue'
|
||||
import type { ColumnsType } from 'ant-design-vue/es/table'
|
||||
import { ReloadOutlined } from '@ant-design/icons-vue'
|
||||
import {
|
||||
getEnergyMeters,
|
||||
getEnergyMeter,
|
||||
recordEnergyConsumption,
|
||||
getEnergyConsumption,
|
||||
type EnergyMeter,
|
||||
type EnergyConsumption
|
||||
} from '@/api/energy'
|
||||
import { getProjectSelectorList } from '@/api/project'
|
||||
|
||||
// 能源类型映射
|
||||
const energyTypeMap: Record<string, string> = {
|
||||
ELECTRICITY: '电力',
|
||||
WATER: '水',
|
||||
GAS: '燃气',
|
||||
CENTRAL_HEATING: '集中供热',
|
||||
CENTRAL_COOLING: '集中供冷'
|
||||
}
|
||||
|
||||
// 项目选择选项
|
||||
const projectOptions = ref<{ value: string; label: string }[]>([])
|
||||
|
||||
// 计量点选项
|
||||
const meterOptions = ref<{ value: string; label: string; energyType: string }[]>([])
|
||||
|
||||
// 查询参数
|
||||
const queryParams = reactive({
|
||||
projectId: '',
|
||||
meterId: ''
|
||||
})
|
||||
|
||||
// 数据状态
|
||||
const loading = ref(false)
|
||||
const recordLoading = ref(false)
|
||||
const consumptionList = ref<EnergyConsumption[]>([])
|
||||
const selectedMeter = ref<EnergyMeter | null>(null)
|
||||
const lastRecord = ref<EnergyConsumption | null>(null)
|
||||
|
||||
// 录入表单
|
||||
const formState = reactive({
|
||||
meterId: '',
|
||||
currentReading: undefined as number | undefined,
|
||||
recordedBy: ''
|
||||
})
|
||||
|
||||
// 计算消耗量和费用
|
||||
const calculatedConsumption = computed(() => {
|
||||
if (!selectedMeter.value || formState.currentReading === undefined || lastRecord.value === null) {
|
||||
return { consumption: 0, amount: 0 }
|
||||
}
|
||||
const consumption = formState.currentReading - lastRecord.value.currentReading
|
||||
const amount = consumption * (selectedMeter.value.unitPrice || 0)
|
||||
return { consumption: Math.max(0, consumption), amount }
|
||||
})
|
||||
|
||||
// 表格列定义
|
||||
const columns: ColumnsType = [
|
||||
{ title: '记录日期', dataIndex: 'consumptionDate', key: 'consumptionDate', width: 120 },
|
||||
{ title: '上次读数', dataIndex: 'previousReading', key: 'previousReading', width: 120 },
|
||||
{ title: '当前读数', dataIndex: 'currentReading', key: 'currentReading', width: 120 },
|
||||
{ title: '消耗量', dataIndex: 'consumption', key: 'consumption', width: 100 },
|
||||
{ title: '费用(元)', dataIndex: 'amount', key: 'amount', width: 100 },
|
||||
{ title: '记录方式', dataIndex: 'recordMethod', key: 'recordMethod', width: 100 }
|
||||
]
|
||||
|
||||
// 获取项目列表
|
||||
const fetchProjects = async () => {
|
||||
try {
|
||||
const res = await getProjectSelectorList()
|
||||
projectOptions.value = (res.data.data || []).map((item: any) => ({
|
||||
value: item.id,
|
||||
label: item.name
|
||||
}))
|
||||
} catch {
|
||||
message.error('获取项目列表失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 获取计量点列表
|
||||
const fetchMeters = async () => {
|
||||
if (!queryParams.projectId) {
|
||||
meterOptions.value = []
|
||||
return
|
||||
}
|
||||
try {
|
||||
const res = await getEnergyMeters(queryParams.projectId)
|
||||
const data = res.data.data
|
||||
const meters: EnergyMeter[] = data.content || data || []
|
||||
meterOptions.value = meters.map(m => ({
|
||||
value: m.id!,
|
||||
label: `${m.meterName} (${m.meterCode})`,
|
||||
energyType: m.energyType
|
||||
}))
|
||||
queryParams.meterId = ''
|
||||
selectedMeter.value = null
|
||||
lastRecord.value = null
|
||||
formState.currentReading = undefined
|
||||
} catch {
|
||||
message.error('获取计量点列表失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 获取计量点详情和上条记录
|
||||
const fetchMeterDetail = async () => {
|
||||
if (!queryParams.meterId) {
|
||||
selectedMeter.value = null
|
||||
lastRecord.value = null
|
||||
return
|
||||
}
|
||||
try {
|
||||
const meterRes = await getEnergyMeter(queryParams.meterId)
|
||||
selectedMeter.value = meterRes.data.data
|
||||
|
||||
// 获取历史记录找最近一条
|
||||
const historyRes = await getEnergyConsumption(queryParams.meterId)
|
||||
const history = historyRes.data.data || []
|
||||
if (history.length > 0) {
|
||||
lastRecord.value = history[0]
|
||||
} else {
|
||||
lastRecord.value = null
|
||||
}
|
||||
} catch {
|
||||
message.error('获取计量点详情失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 获取历史记录
|
||||
const fetchHistory = async () => {
|
||||
if (!queryParams.meterId) return
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await getEnergyConsumption(queryParams.meterId)
|
||||
consumptionList.value = res.data.data || []
|
||||
} catch {
|
||||
message.error('获取能耗记录失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 选择项目
|
||||
const handleProjectChange = () => {
|
||||
queryParams.meterId = ''
|
||||
selectedMeter.value = null
|
||||
lastRecord.value = null
|
||||
formState.currentReading = undefined
|
||||
consumptionList.value = []
|
||||
fetchMeters()
|
||||
}
|
||||
|
||||
// 选择计量点
|
||||
const handleMeterChange = () => {
|
||||
fetchMeterDetail()
|
||||
fetchHistory()
|
||||
}
|
||||
|
||||
// 提交记录
|
||||
const handleSubmit = async () => {
|
||||
if (!formState.meterId) {
|
||||
message.warning('请选择计量点')
|
||||
return
|
||||
}
|
||||
if (formState.currentReading === undefined) {
|
||||
message.warning('请输入当前读数')
|
||||
return
|
||||
}
|
||||
if (lastRecord.value && formState.currentReading < lastRecord.value.currentReading) {
|
||||
message.warning('当前读数不能小于上次读数')
|
||||
return
|
||||
}
|
||||
recordLoading.value = true
|
||||
try {
|
||||
await recordEnergyConsumption({
|
||||
meterId: formState.meterId,
|
||||
currentReading: formState.currentReading!,
|
||||
recordedBy: formState.recordedBy
|
||||
})
|
||||
message.success('录入成功')
|
||||
// 刷新数据
|
||||
fetchMeterDetail()
|
||||
fetchHistory()
|
||||
formState.currentReading = undefined
|
||||
} catch {
|
||||
message.error('录入失败')
|
||||
} finally {
|
||||
recordLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 重置
|
||||
const handleReset = () => {
|
||||
queryParams.projectId = ''
|
||||
queryParams.meterId = ''
|
||||
selectedMeter.value = null
|
||||
lastRecord.value = null
|
||||
formState.currentReading = undefined
|
||||
formState.recordedBy = ''
|
||||
consumptionList.value = []
|
||||
meterOptions.value = []
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchProjects()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<!-- 页面标题 -->
|
||||
<div class="page-header">
|
||||
<h2 class="page-title">能耗录入</h2>
|
||||
</div>
|
||||
|
||||
<Row :gutter="24">
|
||||
<!-- 左侧:录入表单 -->
|
||||
<Col :span="10">
|
||||
<Card title="录入能耗数据" class="record-card">
|
||||
<!-- 筛选区 -->
|
||||
<div class="filter-section">
|
||||
<Form layout="vertical">
|
||||
<Form.Item label="选择项目">
|
||||
<Select
|
||||
v-model:value="queryParams.projectId"
|
||||
placeholder="请选择项目"
|
||||
style="width: 100%"
|
||||
:options="projectOptions"
|
||||
@change="handleProjectChange"
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item label="选择计量点">
|
||||
<Select
|
||||
v-model:value="queryParams.meterId"
|
||||
placeholder="请选择计量点"
|
||||
style="width: 100%"
|
||||
:options="meterOptions"
|
||||
@change="handleMeterChange"
|
||||
/>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</div>
|
||||
|
||||
<!-- 计量点信息 -->
|
||||
<div v-if="selectedMeter" class="meter-info">
|
||||
<Row :gutter="16">
|
||||
<Col :span="12">
|
||||
<Statistic title="能源类型" :value="energyTypeMap[selectedMeter.energyType] || selectedMeter.energyType" />
|
||||
</Col>
|
||||
<Col :span="12">
|
||||
<Statistic title="单价" :value="selectedMeter.unitPrice || 0" suffix="元" :precision="2" />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row :gutter="16" style="margin-top: 16px">
|
||||
<Col :span="12">
|
||||
<Statistic title="额定容量" :value="selectedMeter.ratedCapacity || 0" suffix="kW" />
|
||||
</Col>
|
||||
<Col :span="12">
|
||||
<Statistic title="安装位置" :value="selectedMeter.installationLocation || '-'" />
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
|
||||
<!-- 上次记录 -->
|
||||
<div v-if="lastRecord" class="last-record">
|
||||
<h4>上次记录</h4>
|
||||
<Row :gutter="16">
|
||||
<Col :span="12">
|
||||
<Statistic title="记录日期" :value="lastRecord.consumptionDate" />
|
||||
</Col>
|
||||
<Col :span="12">
|
||||
<Statistic title="上次读数" :value="lastRecord.currentReading" />
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
|
||||
<!-- 录入表单 -->
|
||||
<div class="record-form">
|
||||
<Form layout="vertical">
|
||||
<Form.Item label="当前读数">
|
||||
<InputNumber
|
||||
v-model:value="formState.currentReading"
|
||||
placeholder="请输入当前读数"
|
||||
style="width: 100%"
|
||||
:min="0"
|
||||
:precision="2"
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item label="记录人">
|
||||
<Select
|
||||
v-model:value="formState.recordedBy"
|
||||
placeholder="请输入记录人"
|
||||
style="width: 100%"
|
||||
allow-clear
|
||||
/>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
|
||||
<!-- 计算结果预览 -->
|
||||
<div v-if="formState.currentReading !== undefined" class="calculation-preview">
|
||||
<Row :gutter="16">
|
||||
<Col :span="12">
|
||||
<Statistic title="消耗量" :value="calculatedConsumption.consumption" :precision="2" />
|
||||
</Col>
|
||||
<Col :span="12">
|
||||
<Statistic title="费用" :value="calculatedConsumption.amount" suffix="元" :precision="2" />
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
|
||||
<Space style="width: 100%; justify-content: flex-end; margin-top: 16px">
|
||||
<Button @click="handleReset">
|
||||
<ReloadOutlined /> 重置
|
||||
</Button>
|
||||
<Button type="primary" :loading="recordLoading" @click="handleSubmit">
|
||||
提交记录
|
||||
</Button>
|
||||
</Space>
|
||||
</div>
|
||||
</Card>
|
||||
</Col>
|
||||
|
||||
<!-- 右侧:历史记录 -->
|
||||
<Col :span="14">
|
||||
<Card title="历史记录" class="history-card">
|
||||
<a-table
|
||||
:columns="columns"
|
||||
:data-source="consumptionList"
|
||||
:loading="loading"
|
||||
:row-key="(record: EnergyConsumption) => record.id || record.consumptionDate"
|
||||
:pagination="{
|
||||
pageSize: 10,
|
||||
showSizeChanger: true,
|
||||
showTotal: (total: number) => `共 ${total} 条`
|
||||
}"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'consumption'">
|
||||
{{ record.consumption }} kWh
|
||||
</template>
|
||||
<template v-else-if="column.key === 'amount'">
|
||||
{{ record.amount ? `¥${record.amount.toFixed(2)}` : '-' }}
|
||||
</template>
|
||||
<template v-else-if="column.key === 'recordMethod'">
|
||||
{{ record.recordMethod === 'MANUAL' ? '手动录入' : '自动抄表' }}
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
<a-empty v-if="!queryParams.meterId" description="请先选择计量点" />
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.page-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
margin: 0;
|
||||
font-size: 20px;
|
||||
font-weight: 500;
|
||||
color: #262626;
|
||||
}
|
||||
|
||||
.record-card,
|
||||
.history-card {
|
||||
height: calc(100vh - 200px);
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.filter-section {
|
||||
margin-bottom: 24px;
|
||||
padding-bottom: 24px;
|
||||
border-bottom: 1px solid #e8e8e8;
|
||||
}
|
||||
|
||||
.meter-info {
|
||||
margin-bottom: 24px;
|
||||
padding: 16px;
|
||||
background: #fafafa;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.last-record {
|
||||
margin-bottom: 24px;
|
||||
padding: 16px;
|
||||
background: #f6ffed;
|
||||
border: 1px solid #b7eb8f;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.last-record h4 {
|
||||
margin: 0 0 16px 0;
|
||||
color: #52c41a;
|
||||
}
|
||||
|
||||
.record-form {
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
.calculation-preview {
|
||||
margin-top: 16px;
|
||||
padding: 16px;
|
||||
background: #e6f7ff;
|
||||
border: 1px solid #91d5ff;
|
||||
border-radius: 8px;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,352 @@
|
|||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted, watch } from 'vue'
|
||||
import { Button, Select, Space, message, Row, Col, Card, Statistic, Table, DatePicker } from 'ant-design-vue'
|
||||
import type { ColumnsType } from 'ant-design-vue/es/table'
|
||||
import { SearchOutlined, ReloadOutlined } from '@ant-design/icons-vue'
|
||||
import { getConsumptionByType, getUnitConsumption } from '@/api/energy'
|
||||
import { getProjectSelectorList } from '@/api/project'
|
||||
|
||||
// 能源类型映射
|
||||
const energyTypeMap: Record<string, { color: string; text: string }> = {
|
||||
ELECTRICITY: { color: '#faad14', text: '电力' },
|
||||
WATER: { color: '#1890ff', text: '水' },
|
||||
GAS: { color: '#fa541c', text: '燃气' },
|
||||
CENTRAL_HEATING: { color: '#f5222d', text: '集中供热' },
|
||||
CENTRAL_COOLING: { color: '#13c2c2', text: '集中供冷' }
|
||||
}
|
||||
|
||||
// 项目选择选项
|
||||
const projectOptions = ref<{ value: string; label: string }[]>([])
|
||||
|
||||
// 查询参数
|
||||
const queryParams = reactive({
|
||||
projectId: '',
|
||||
month: ''
|
||||
})
|
||||
|
||||
// 数据状态
|
||||
const loading = ref(false)
|
||||
const byTypeData = ref<{ energyType: string; consumption: number; amount: number }[]>([])
|
||||
const unitData = ref<{ indicatorName: string; value: number; unit: string }[]>([])
|
||||
|
||||
// 总计
|
||||
const totalConsumption = ref(0)
|
||||
const totalAmount = ref(0)
|
||||
|
||||
// 表格列定义(分项统计)
|
||||
const byTypeColumns: ColumnsType = [
|
||||
{ title: '能源类型', dataIndex: 'energyType', key: 'energyType', width: 150 },
|
||||
{ title: '消耗量(kWh)', dataIndex: 'consumption', key: 'consumption', width: 150 },
|
||||
{ title: '费用(元)', dataIndex: 'amount', key: 'amount', width: 150 },
|
||||
{
|
||||
title: '占比',
|
||||
key: 'percentage',
|
||||
width: 150,
|
||||
customRender: ({ record }: { record: { consumption: number } }) => {
|
||||
const pct = totalConsumption.value > 0
|
||||
? ((record.consumption / totalConsumption.value) * 100).toFixed(1)
|
||||
: '0.0'
|
||||
return `${pct}%`
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
// 单方能耗列定义
|
||||
const unitColumns: ColumnsType = [
|
||||
{ title: '指标名称', dataIndex: 'indicatorName', key: 'indicatorName', width: 200 },
|
||||
{ title: '指标值', dataIndex: 'value', key: 'value', width: 150 },
|
||||
{ title: '单位', dataIndex: 'unit', key: 'unit', width: 100 }
|
||||
]
|
||||
|
||||
// 获取项目列表
|
||||
const fetchProjects = async () => {
|
||||
try {
|
||||
const res = await getProjectSelectorList()
|
||||
projectOptions.value = (res.data.data || []).map((item: any) => ({
|
||||
value: item.id,
|
||||
label: item.name
|
||||
}))
|
||||
} catch {
|
||||
message.error('获取项目列表失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 获取月度格式化
|
||||
const getCurrentMonth = () => {
|
||||
const now = new Date()
|
||||
const year = now.getFullYear()
|
||||
const month = String(now.getMonth() + 1).padStart(2, '0')
|
||||
return `${year}-${month}`
|
||||
}
|
||||
|
||||
// 获取分项统计数据
|
||||
const fetchByTypeData = async () => {
|
||||
if (!queryParams.projectId || !queryParams.month) return
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await getConsumptionByType(queryParams.projectId, queryParams.month)
|
||||
byTypeData.value = res.data.data || []
|
||||
// 计算总计
|
||||
totalConsumption.value = byTypeData.value.reduce((sum, item) => sum + item.consumption, 0)
|
||||
totalAmount.value = byTypeData.value.reduce((sum, item) => sum + item.amount, 0)
|
||||
} catch {
|
||||
message.error('获取分项统计数据失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 获取单方能耗数据
|
||||
const fetchUnitData = async () => {
|
||||
if (!queryParams.projectId || !queryParams.month) return
|
||||
try {
|
||||
const res = await getUnitConsumption(queryParams.projectId, queryParams.month)
|
||||
unitData.value = res.data.data || []
|
||||
} catch {
|
||||
message.error('获取单方能耗数据失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 搜索
|
||||
const handleSearch = () => {
|
||||
if (!queryParams.projectId) {
|
||||
message.warning('请先选择项目')
|
||||
return
|
||||
}
|
||||
if (!queryParams.month) {
|
||||
message.warning('请选择月份')
|
||||
return
|
||||
}
|
||||
fetchByTypeData()
|
||||
fetchUnitData()
|
||||
}
|
||||
|
||||
// 重置
|
||||
const handleReset = () => {
|
||||
queryParams.projectId = ''
|
||||
queryParams.month = ''
|
||||
byTypeData.value = []
|
||||
unitData.value = []
|
||||
totalConsumption.value = 0
|
||||
totalAmount.value = 0
|
||||
}
|
||||
|
||||
// 获取能源类型标签
|
||||
const getEnergyTypeTag = (type: string) => {
|
||||
return energyTypeMap[type] || { color: 'default', text: type }
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchProjects()
|
||||
queryParams.month = getCurrentMonth()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<!-- 页面标题 -->
|
||||
<div class="page-header">
|
||||
<h2 class="page-title">能耗统计</h2>
|
||||
</div>
|
||||
|
||||
<!-- 筛选区 -->
|
||||
<div class="filter-bar">
|
||||
<Space>
|
||||
<Select
|
||||
v-model:value="queryParams.projectId"
|
||||
placeholder="请选择项目"
|
||||
style="width: 240px"
|
||||
allow-clear
|
||||
:options="projectOptions"
|
||||
/>
|
||||
<DatePicker
|
||||
v-model:value="queryParams.month"
|
||||
picker="month"
|
||||
placeholder="选择月份"
|
||||
style="width: 140px"
|
||||
:format="(value: any) => value ? value.format('YYYY-MM') : ''"
|
||||
/>
|
||||
<Button type="primary" @click="handleSearch">
|
||||
<SearchOutlined /> 查询
|
||||
</Button>
|
||||
<Button @click="handleReset">
|
||||
<ReloadOutlined /> 重置
|
||||
</Button>
|
||||
</Space>
|
||||
</div>
|
||||
|
||||
<!-- 统计卡片 -->
|
||||
<Row :gutter="16" style="margin-bottom: 24px">
|
||||
<Col :span="6">
|
||||
<Card>
|
||||
<Statistic
|
||||
title="月度总消耗"
|
||||
:value="totalConsumption"
|
||||
suffix="kWh"
|
||||
:precision="2"
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col :span="6">
|
||||
<Card>
|
||||
<Statistic
|
||||
title="月度总费用"
|
||||
:value="totalAmount"
|
||||
prefix="¥"
|
||||
:precision="2"
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col :span="6">
|
||||
<Card>
|
||||
<Statistic
|
||||
title="计量点数量"
|
||||
:value="byTypeData.length"
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col :span="6">
|
||||
<Card>
|
||||
<Statistic
|
||||
title="统计月份"
|
||||
:value="queryParams.month || '-'"
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<!-- 分项统计饼图区域 -->
|
||||
<Row :gutter="24" style="margin-bottom: 24px">
|
||||
<Col :span="24">
|
||||
<Card title="分项能耗构成">
|
||||
<!-- 简单饼图实现 -->
|
||||
<div class="pie-container">
|
||||
<div class="pie-chart">
|
||||
<div
|
||||
v-for="(item, index) in byTypeData"
|
||||
:key="item.energyType"
|
||||
class="pie-segment"
|
||||
:style="{
|
||||
'--percentage': `${(item.consumption / totalConsumption) * 100 || 0}%`,
|
||||
'--color': getEnergyTypeTag(item.energyType).color,
|
||||
'--index': index
|
||||
}"
|
||||
>
|
||||
<div class="pie-label">
|
||||
<span class="pie-color" :style="{ background: getEnergyTypeTag(item.energyType).color }"></span>
|
||||
<span class="pie-text">{{ getEnergyTypeTag(item.energyType).text }}</span>
|
||||
<span class="pie-value">{{ ((item.consumption / totalConsumption) * 100 || 0).toFixed(1) }}%</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Row :gutter="24">
|
||||
<!-- 分项能耗表格 -->
|
||||
<Col :span="12">
|
||||
<Card title="分项能耗明细">
|
||||
<a-table
|
||||
:columns="byTypeColumns"
|
||||
:data-source="byTypeData"
|
||||
:loading="loading"
|
||||
:row-key="(record: any) => record.energyType"
|
||||
:pagination="false"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'energyType'">
|
||||
<span :style="{ color: getEnergyTypeTag(record.energyType).color }">
|
||||
{{ getEnergyTypeTag(record.energyType).text }}
|
||||
</span>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
<a-empty v-if="!queryParams.projectId || byTypeData.length === 0" description="请先选择项目和月份进行查询" />
|
||||
</Card>
|
||||
</Col>
|
||||
|
||||
<!-- 单方能耗指标 -->
|
||||
<Col :span="12">
|
||||
<Card title="单方能耗指标">
|
||||
<a-table
|
||||
:columns="unitColumns"
|
||||
:data-source="unitData"
|
||||
:loading="loading"
|
||||
:row-key="(record: any) => record.indicatorName"
|
||||
:pagination="false"
|
||||
/>
|
||||
<a-empty v-if="!queryParams.projectId || unitData.length === 0" description="请先选择项目和月份进行查询" />
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.page-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
margin: 0;
|
||||
font-size: 20px;
|
||||
font-weight: 500;
|
||||
color: #262626;
|
||||
}
|
||||
|
||||
.filter-bar {
|
||||
margin-bottom: 24px;
|
||||
padding: 16px;
|
||||
background: #fafafa;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
/* 简单饼图样式 */
|
||||
.pie-container {
|
||||
padding: 20px 0;
|
||||
}
|
||||
|
||||
.pie-chart {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 16px;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.pie-segment {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 8px 16px;
|
||||
border-left: 4px solid var(--color);
|
||||
background: #fafafa;
|
||||
border-radius: 4px;
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
.pie-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.pie-color {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.pie-text {
|
||||
font-weight: 500;
|
||||
color: #262626;
|
||||
}
|
||||
|
||||
.pie-value {
|
||||
color: #8c8c8c;
|
||||
margin-left: 8px;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,380 @@
|
|||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { Button, Select, Space, message, Tag, Modal, Form, Input, InputNumber, Popconfirm } from 'ant-design-vue'
|
||||
import type { ColumnsType } from 'ant-design-vue/es/table'
|
||||
import {
|
||||
SearchOutlined,
|
||||
ReloadOutlined,
|
||||
PlusOutlined
|
||||
} from '@ant-design/icons-vue'
|
||||
import {
|
||||
getEnergyMeters,
|
||||
createEnergyMeter,
|
||||
updateEnergyMeter,
|
||||
deleteEnergyMeter,
|
||||
type EnergyMeter
|
||||
} from '@/api/energy'
|
||||
import { getProjectSelectorList } from '@/api/project'
|
||||
import { TableActions, Pagination } from '@/components'
|
||||
|
||||
// 能源类型映射
|
||||
const energyTypeMap: Record<string, { color: string; text: string }> = {
|
||||
ELECTRICITY: { color: 'gold', text: '电力' },
|
||||
WATER: { color: 'blue', text: '水' },
|
||||
GAS: { color: 'orange', text: '燃气' },
|
||||
CENTRAL_HEATING: { color: 'red', text: '集中供热' },
|
||||
CENTRAL_COOLING: { color: 'cyan', text: '集中供冷' }
|
||||
}
|
||||
|
||||
// 表格列定义
|
||||
const columns: ColumnsType = [
|
||||
{ title: '计量点编码', dataIndex: 'meterCode', key: 'meterCode', width: 150 },
|
||||
{ title: '计量点名称', dataIndex: 'meterName', key: 'meterName', width: 180 },
|
||||
{ title: '能源类型', dataIndex: 'energyType', key: 'energyType', width: 100 },
|
||||
{ title: '安装位置', dataIndex: 'installationLocation', key: 'installationLocation', width: 150 },
|
||||
{ title: '额定容量', dataIndex: 'ratedCapacity', key: 'ratedCapacity', width: 100 },
|
||||
{ title: '单价(元)', dataIndex: 'unitPrice', key: 'unitPrice', width: 100 },
|
||||
{ title: '状态', dataIndex: 'status', key: 'status', width: 80 },
|
||||
{ title: '操作', key: 'action', width: 120, fixed: 'right' as const }
|
||||
]
|
||||
|
||||
// 项目选择选项
|
||||
const projectOptions = ref<{ value: string; label: string }[]>([])
|
||||
|
||||
// 能源类型选项
|
||||
const energyTypeOptions = [
|
||||
{ value: 'ELECTRICITY', label: '电力' },
|
||||
{ value: 'WATER', label: '水' },
|
||||
{ value: 'GAS', label: '燃气' },
|
||||
{ value: 'CENTRAL_HEATING', label: '集中供热' },
|
||||
{ value: 'CENTRAL_COOLING', label: '集中供冷' }
|
||||
]
|
||||
|
||||
// 查询参数
|
||||
const queryParams = reactive({
|
||||
projectId: '',
|
||||
energyType: undefined as string | undefined
|
||||
})
|
||||
|
||||
// 数据状态
|
||||
const loading = ref(false)
|
||||
const tableData = ref<EnergyMeter[]>([])
|
||||
const pagination = reactive({
|
||||
current: 1,
|
||||
pageSize: 10,
|
||||
total: 0
|
||||
})
|
||||
|
||||
// 新建/编辑 Modal
|
||||
const modalVisible = ref(false)
|
||||
const modalTitle = ref('新建计量点')
|
||||
const modalLoading = ref(false)
|
||||
const editingRecord = ref<EnergyMeter | null>(null)
|
||||
const formState = reactive<EnergyMeter>({
|
||||
meterCode: '',
|
||||
meterName: '',
|
||||
energyType: 'ELECTRICITY',
|
||||
installationLocation: '',
|
||||
ratedCapacity: undefined,
|
||||
unitPrice: undefined,
|
||||
status: 'ACTIVE'
|
||||
})
|
||||
|
||||
// 表单验证
|
||||
const rules = {
|
||||
meterCode: [{ required: true, message: '请输入计量点编码' }],
|
||||
meterName: [{ required: true, message: '请输入计量点名称' }],
|
||||
energyType: [{ required: true, message: '请选择能源类型' }]
|
||||
}
|
||||
|
||||
// 获取项目列表
|
||||
const fetchProjects = async () => {
|
||||
try {
|
||||
const res = await getProjectSelectorList()
|
||||
projectOptions.value = (res.data.data || []).map((item: any) => ({
|
||||
value: item.id,
|
||||
label: item.name
|
||||
}))
|
||||
} catch {
|
||||
message.error('获取项目列表失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 获取计量点列表
|
||||
const fetchMeterList = async () => {
|
||||
if (!queryParams.projectId) {
|
||||
message.warning('请先选择项目')
|
||||
return
|
||||
}
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await getEnergyMeters(queryParams.projectId, queryParams.energyType)
|
||||
const data = res.data.data
|
||||
tableData.value = data.content || data || []
|
||||
pagination.total = data.totalElements || (Array.isArray(data) ? data.length : 0)
|
||||
} catch {
|
||||
message.error('获取计量点列表失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 搜索
|
||||
const handleSearch = () => {
|
||||
pagination.current = 1
|
||||
fetchMeterList()
|
||||
}
|
||||
|
||||
// 重置
|
||||
const handleReset = () => {
|
||||
queryParams.projectId = ''
|
||||
queryParams.energyType = undefined
|
||||
pagination.current = 1
|
||||
tableData.value = []
|
||||
}
|
||||
|
||||
// 分页变化
|
||||
const handlePageChange = (page: number, pageSize: number) => {
|
||||
pagination.current = page
|
||||
pagination.pageSize = pageSize
|
||||
fetchMeterList()
|
||||
}
|
||||
|
||||
// 打开新建 Modal
|
||||
const handleAdd = () => {
|
||||
editingRecord.value = null
|
||||
modalTitle.value = '新建计量点'
|
||||
Object.assign(formState, {
|
||||
id: undefined,
|
||||
meterCode: '',
|
||||
meterName: '',
|
||||
energyType: 'ELECTRICITY',
|
||||
installationLocation: '',
|
||||
ratedCapacity: undefined,
|
||||
unitPrice: undefined,
|
||||
status: 'ACTIVE'
|
||||
})
|
||||
modalVisible.value = true
|
||||
}
|
||||
|
||||
// 打开编辑 Modal
|
||||
const handleEdit = (record: EnergyMeter) => {
|
||||
editingRecord.value = record
|
||||
modalTitle.value = '编辑计量点'
|
||||
Object.assign(formState, {
|
||||
id: record.id,
|
||||
meterCode: record.meterCode,
|
||||
meterName: record.meterName,
|
||||
energyType: record.energyType,
|
||||
installationLocation: record.installationLocation || '',
|
||||
ratedCapacity: record.ratedCapacity,
|
||||
unitPrice: record.unitPrice,
|
||||
status: record.status || 'ACTIVE'
|
||||
})
|
||||
modalVisible.value = true
|
||||
}
|
||||
|
||||
// 删除计量点
|
||||
const handleDelete = async (id: string) => {
|
||||
try {
|
||||
await deleteEnergyMeter(id)
|
||||
message.success('删除成功')
|
||||
fetchMeterList()
|
||||
} catch {
|
||||
message.error('删除失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 提交表单
|
||||
const handleSubmit = async () => {
|
||||
modalLoading.value = true
|
||||
try {
|
||||
if (editingRecord.value) {
|
||||
await updateEnergyMeter(editingRecord.value.id!, formState)
|
||||
message.success('更新成功')
|
||||
} else {
|
||||
await createEnergyMeter({ ...formState, status: 'ACTIVE' } as EnergyMeter)
|
||||
message.success('创建成功')
|
||||
}
|
||||
modalVisible.value = false
|
||||
fetchMeterList()
|
||||
} catch {
|
||||
message.error(editingRecord.value ? '更新失败' : '创建失败')
|
||||
} finally {
|
||||
modalLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 获取能源类型标签
|
||||
const getEnergyTypeTag = (type: string) => {
|
||||
const config = energyTypeMap[type] || { color: 'default', text: type }
|
||||
return config
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchProjects()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<!-- 页面标题 -->
|
||||
<div class="page-header">
|
||||
<h2 class="page-title">计量点管理</h2>
|
||||
</div>
|
||||
|
||||
<!-- 筛选区 -->
|
||||
<div class="filter-bar">
|
||||
<Space>
|
||||
<Select
|
||||
v-model:value="queryParams.projectId"
|
||||
placeholder="请选择项目"
|
||||
style="width: 240px"
|
||||
allow-clear
|
||||
:options="projectOptions"
|
||||
/>
|
||||
<Select
|
||||
v-model:value="queryParams.energyType"
|
||||
placeholder="能源类型"
|
||||
style="width: 140px"
|
||||
allow-clear
|
||||
:options="energyTypeOptions"
|
||||
/>
|
||||
<Button type="primary" @click="handleSearch">
|
||||
<SearchOutlined /> 查询
|
||||
</Button>
|
||||
<Button @click="handleReset">
|
||||
<ReloadOutlined /> 重置
|
||||
</Button>
|
||||
</Space>
|
||||
</div>
|
||||
|
||||
<!-- 表格区 -->
|
||||
<div class="table-card">
|
||||
<div class="table-toolbar">
|
||||
<Button type="primary" @click="handleAdd">
|
||||
<PlusOutlined /> 新建计量点
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<a-table
|
||||
:columns="columns"
|
||||
:data-source="tableData"
|
||||
:loading="loading"
|
||||
:row-key="(record: EnergyMeter) => record.id || record.meterCode"
|
||||
:pagination="{
|
||||
current: pagination.current,
|
||||
pageSize: pagination.pageSize,
|
||||
total: pagination.total,
|
||||
showSizeChanger: true,
|
||||
showQuickJumper: true,
|
||||
showTotal: (total: number) => `共 ${total} 条`
|
||||
}"
|
||||
@change="(pag: any) => handlePageChange(pag.current, pag.pageSize)"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'energyType'">
|
||||
<Tag :color="getEnergyTypeTag(record.energyType).color">
|
||||
{{ getEnergyTypeTag(record.energyType).text }}
|
||||
</Tag>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'ratedCapacity'">
|
||||
{{ record.ratedCapacity ? `${record.ratedCapacity} kW` : '-' }}
|
||||
</template>
|
||||
<template v-else-if="column.key === 'unitPrice'">
|
||||
{{ record.unitPrice ? `¥${record.unitPrice}` : '-' }}
|
||||
</template>
|
||||
<template v-else-if="column.key === 'status'">
|
||||
<Tag :color="record.status === 'ACTIVE' ? 'green' : 'red'">
|
||||
{{ record.status === 'ACTIVE' ? '启用' : '停用' }}
|
||||
</Tag>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'action'">
|
||||
<Space>
|
||||
<Button type="link" size="small" @click="handleEdit(record)">编辑</Button>
|
||||
<Popconfirm title="确定删除该计量点?" @confirm="handleDelete(record.id!)">
|
||||
<Button type="link" size="small" danger>删除</Button>
|
||||
</Popconfirm>
|
||||
</Space>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
|
||||
<!-- 未选择项目提示 -->
|
||||
<a-empty v-if="!queryParams.projectId && tableData.length === 0" description="请先选择项目" />
|
||||
</div>
|
||||
|
||||
<!-- 新建/编辑 Modal -->
|
||||
<Modal
|
||||
v-model:open="modalVisible"
|
||||
:title="modalTitle"
|
||||
:confirm-loading="modalLoading"
|
||||
@ok="handleSubmit"
|
||||
@cancel="modalVisible = false"
|
||||
width="560px"
|
||||
>
|
||||
<Form
|
||||
:model="formState"
|
||||
:rules="rules"
|
||||
layout="vertical"
|
||||
class="meter-form"
|
||||
>
|
||||
<Form.Item label="计量点编码" name="meterCode">
|
||||
<Input v-model:value="formState.meterCode" placeholder="请输入计量点编码" />
|
||||
</Form.Item>
|
||||
<Form.Item label="计量点名称" name="meterName">
|
||||
<Input v-model:value="formState.meterName" placeholder="请输入计量点名称" />
|
||||
</Form.Item>
|
||||
<Form.Item label="能源类型" name="energyType">
|
||||
<Select v-model:value="formState.energyType" :options="energyTypeOptions" />
|
||||
</Form.Item>
|
||||
<Form.Item label="安装位置" name="installationLocation">
|
||||
<Input v-model:value="formState.installationLocation" placeholder="请输入安装位置" />
|
||||
</Form.Item>
|
||||
<Form.Item label="额定容量(kW)" name="ratedCapacity">
|
||||
<InputNumber v-model:value="formState.ratedCapacity" placeholder="请输入额定容量" style="width: 100%" />
|
||||
</Form.Item>
|
||||
<Form.Item label="单价(元)" name="unitPrice">
|
||||
<InputNumber v-model:value="formState.unitPrice" placeholder="请输入单价" style="width: 100%" :precision="2" />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.page-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
margin: 0;
|
||||
font-size: 20px;
|
||||
font-weight: 500;
|
||||
color: #262626;
|
||||
}
|
||||
|
||||
.filter-bar {
|
||||
margin-bottom: 24px;
|
||||
padding: 16px;
|
||||
background: #fafafa;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.table-card {
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.table-toolbar {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.meter-form {
|
||||
margin-top: 16px;
|
||||
}
|
||||
</style>
|
||||
Loading…
Reference in New Issue