Files
klp-oa/klp-ui/src/views/ems/meter/manage.vue

828 lines
22 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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