feat: add energy monitoring frontend pages

This commit is contained in:
chiguyong 2026-03-24 00:34:43 +08:00
parent 913d6400e4
commit 5500238be3
6 changed files with 1271 additions and 0 deletions

93
src/api/energy.ts Normal file
View File

@ -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 }
})
}

View File

@ -92,6 +92,24 @@ const router = createRouter({
name: 'MaintenanceTasks', name: 'MaintenanceTasks',
component: () => import('@/views/maintenance/TaskList.vue'), component: () => import('@/views/maintenance/TaskList.vue'),
meta: { title: '维保任务' } 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: '能耗统计' }
} }
] ]
} }

View File

@ -10,6 +10,7 @@ import {
TeamOutlined, TeamOutlined,
AppstoreOutlined, AppstoreOutlined,
BuildOutlined, BuildOutlined,
HeatMapOutlined,
LogoutOutlined, LogoutOutlined,
AuditOutlined, AuditOutlined,
SettingOutlined, SettingOutlined,
@ -55,6 +56,16 @@ const menuItems: MenuProps['items'] = [
key: '/maintenance/tasks', key: '/maintenance/tasks',
icon: () => h(ToolOutlined), icon: () => h(ToolOutlined),
label: '维保任务' label: '维保任务'
},
{
key: 'energy',
icon: () => h(HeatMapOutlined),
label: '能耗管理',
children: [
{ key: '/energy/meters', label: '计量点管理' },
{ key: '/energy/consumption', label: '能耗录入' },
{ key: '/energy/statistics', label: '能耗统计' }
]
} }
] ]
}, },

View File

@ -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>

View File

@ -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>

View File

@ -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>