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

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