<template>
|
<div class="app-container">
|
<el-card class="sync-card" shadow="never">
|
<el-row :gutter="16">
|
<el-col :span="8">
|
<div class="sync-item">
|
<div class="sync-label">电表数量</div>
|
<div class="sync-value">{{ syncStatus.meterCount ?? 0 }}</div>
|
</div>
|
</el-col>
|
<el-col :span="8">
|
<div class="sync-item">
|
<div class="sync-label">最近同步时间</div>
|
<div class="sync-value sync-time">{{ lastHourSync }}</div>
|
</div>
|
</el-col>
|
<el-col :span="8">
|
<div class="sync-item">
|
<div class="sync-label">电量记录数</div>
|
<div class="sync-value">{{ syncStatus.recordCountByDimension?.hour ?? 0 }}</div>
|
</div>
|
</el-col>
|
</el-row>
|
</el-card>
|
|
<el-tabs v-model="activeTab">
|
<el-tab-pane label="电量数据" name="record">
|
<el-card shadow="never">
|
<template #header>
|
<div class="card-header">
|
<span>能耗数据采集</span>
|
<span class="desc">展示同步小时电量,支持手动抄表补录</span>
|
</div>
|
</template>
|
|
<el-form :inline="true" class="search-form">
|
<el-form-item label="时间范围">
|
<el-date-picker
|
v-model="hourRange"
|
type="datetimerange"
|
range-separator="至"
|
start-placeholder="开始时间"
|
end-placeholder="结束时间"
|
value-format="YYYY-MM-DD HH:mm:ss"
|
:default-time="defaultTime"
|
/>
|
</el-form-item>
|
<el-form-item>
|
<el-button type="primary" :loading="loading" @click="handleRefresh">
|
<el-icon><Refresh /></el-icon>
|
刷新
|
</el-button>
|
<el-button type="primary" @click="openRecordForm('add')">手动抄表</el-button>
|
<el-button type="danger" plain :disabled="!selectedRows.length" @click="handleDelete">删除</el-button>
|
</el-form-item>
|
</el-form>
|
|
<el-row :gutter="16" class="stat-row">
|
<el-col :span="8">
|
<div class="stat-item">
|
<div class="stat-label">记录条数</div>
|
<div class="stat-value">{{ tableData.length }}</div>
|
</div>
|
</el-col>
|
<el-col :span="8">
|
<div class="stat-item">
|
<div class="stat-label">总用电量(kWh)</div>
|
<div class="stat-value">{{ totalConsumption }}</div>
|
</div>
|
</el-col>
|
<el-col :span="8">
|
<div class="stat-item">
|
<div class="stat-label">涉及电表</div>
|
<div class="stat-value">{{ meterCount }}</div>
|
</div>
|
</el-col>
|
</el-row>
|
|
<el-table
|
v-loading="loading"
|
:data="pagedData"
|
border
|
stripe
|
height="calc(100vh - 480px)"
|
@selection-change="handleSelectionChange"
|
>
|
<el-table-column type="selection" width="50" />
|
<el-table-column label="时间" min-width="150">
|
<template #default="{ row }">
|
{{ formatRecordTime(row) }}
|
</template>
|
</el-table-column>
|
<el-table-column prop="meterName" label="电表名称" min-width="120" show-overflow-tooltip />
|
<el-table-column prop="meterId" label="电表ID" width="100" />
|
<el-table-column prop="address" label="表地址" min-width="110" show-overflow-tooltip />
|
<el-table-column prop="prevReading" label="上次电量" width="100" />
|
<el-table-column prop="currReading" label="本次电量" width="100" />
|
<el-table-column prop="ratio" label="倍率" width="70" />
|
<el-table-column prop="totalConsumption" label="本次用电量(kWh)" width="130" />
|
<el-table-column label="抄表方式" width="90">
|
<template #default="{ row }">
|
<el-tag :type="row.readingMethod === 'manual' ? 'warning' : 'success'" size="small">
|
{{ row.readingMethod === "manual" ? "手动" : "同步" }}
|
</el-tag>
|
</template>
|
</el-table-column>
|
<el-table-column label="操作" width="80" fixed="right">
|
<template #default="{ row }">
|
<el-button
|
v-if="row.readingMethod === 'manual'"
|
link
|
type="primary"
|
@click="openRecordForm('edit', row)"
|
>编辑</el-button>
|
</template>
|
</el-table-column>
|
</el-table>
|
|
<pagination
|
v-show="tableData.length > 0"
|
:total="tableData.length"
|
:page="page.current"
|
:limit="page.size"
|
:page-sizes="[50, 100, 200, 500]"
|
@pagination="handlePagination"
|
/>
|
</el-card>
|
</el-tab-pane>
|
|
<el-tab-pane label="电表管理" name="meter">
|
<el-card shadow="never">
|
<div class="meter-toolbar">
|
<el-input
|
v-model="meterKeyword"
|
placeholder="搜索电表名称/地址/备注"
|
clearable
|
style="width: 240px"
|
@keyup.enter="loadMeters"
|
/>
|
<el-button type="primary" @click="loadMeters">搜索</el-button>
|
<el-button type="success" :loading="meterSyncing" @click="handleMeterSync">同步电表</el-button>
|
<el-button type="primary" @click="openMeterForm('add')">新增电表</el-button>
|
</div>
|
<el-table v-loading="meterLoading" :data="meterTableData" border stripe height="calc(100vh - 420px)">
|
<el-table-column label="电表名称" min-width="120" show-overflow-tooltip>
|
<template #default="{ row }">{{ row.meterName || row.address || "-" }}</template>
|
</el-table-column>
|
<el-table-column prop="meterId" label="档案ID" width="110" />
|
<el-table-column prop="address" label="表地址" min-width="120" />
|
<el-table-column prop="rate" label="倍率" width="70" />
|
<el-table-column label="来源" width="80">
|
<template #default="{ row }">
|
<el-tag :type="row.source === 'manual' ? 'warning' : 'info'" size="small">
|
{{ row.source === "manual" ? "手动" : "同步" }}
|
</el-tag>
|
</template>
|
</el-table-column>
|
<el-table-column label="继电器" width="80">
|
<template #default="{ row }">
|
<el-tag :type="row.relayState === '1' ? 'success' : 'danger'" size="small">
|
{{ row.relayState === "1" ? "合闸" : "拉闸" }}
|
</el-tag>
|
</template>
|
</el-table-column>
|
<el-table-column prop="description" label="备注" min-width="100" show-overflow-tooltip />
|
<el-table-column label="操作" width="140" fixed="right">
|
<template #default="{ row }">
|
<el-button link type="primary" @click="openMeterForm('edit', row)">编辑</el-button>
|
<el-button v-if="row.source === 'manual'" link type="danger" @click="handleMeterDelete(row)">删除</el-button>
|
</template>
|
</el-table-column>
|
</el-table>
|
<pagination
|
v-show="meterPage.total > 0"
|
:total="meterPage.total"
|
:page="meterPage.current"
|
:limit="meterPage.size"
|
:page-sizes="[50, 100, 200, 500]"
|
@pagination="handleMeterPagination"
|
/>
|
</el-card>
|
</el-tab-pane>
|
</el-tabs>
|
|
<form-dia ref="formDiaRef" @close="handleRefresh" />
|
<meter-form-dia ref="meterFormDiaRef" @close="loadMeters" />
|
</div>
|
</template>
|
|
<script setup>
|
import { computed, onMounted, reactive, ref } from "vue";
|
import { Refresh } from "@element-plus/icons-vue";
|
import { ElMessage, ElMessageBox } from "element-plus";
|
import FormDia from "./components/formDia.vue";
|
import MeterFormDia from "./components/meterFormDia.vue";
|
import { eleRecordDelete, meterListPage, meterSync, meterDelete } from "@/api/energyManagement/tqdianbiao.js";
|
import {
|
listStatisticEle,
|
getSyncStatus,
|
formatMinuteTime,
|
parseTimeKey,
|
getRecentHourRange,
|
} from "@/api/energyManagement/statisticEle.js";
|
|
const activeTab = ref("record");
|
const loading = ref(false);
|
const tableData = ref([]);
|
const syncStatus = ref({});
|
const selectedRows = ref([]);
|
const formDiaRef = ref(null);
|
const meterFormDiaRef = ref(null);
|
|
const page = reactive({ current: 1, size: 500 });
|
const defaultTime = [
|
new Date(2000, 0, 1, 0, 0, 0),
|
new Date(2000, 0, 1, 23, 59, 59),
|
];
|
const hourRange = ref([]);
|
|
const meterLoading = ref(false);
|
const meterSyncing = ref(false);
|
const meterKeyword = ref("");
|
const meterTableData = ref([]);
|
const meterPage = reactive({ current: 1, size: 500, total: 0 });
|
|
const lastHourSync = computed(() => syncStatus.value.lastSyncTimeByType?.hour || "-");
|
|
const totalConsumption = computed(() => {
|
return tableData.value.reduce((sum, item) => sum + (item.totalConsumption || 0), 0).toFixed(2);
|
});
|
|
const meterCount = computed(() => new Set(tableData.value.map((item) => item.meterId)).size);
|
|
const pagedData = computed(() => {
|
const start = (page.current - 1) * page.size;
|
return tableData.value.slice(start, start + page.size);
|
});
|
|
function formatRecordTime(row) {
|
const dim = row.readingMethod === "manual" ? "manual" : "hour";
|
if (row.timeKey?.length === 12) return parseTimeKey(row.timeKey, "manual");
|
return parseTimeKey(row.timeKey, dim);
|
}
|
|
function initDefaultRange() {
|
const now = new Date();
|
const start = new Date(now.getTime() - 7 * 86400000);
|
hourRange.value = [
|
`${start.getFullYear()}-${String(start.getMonth() + 1).padStart(2, "0")}-${String(start.getDate()).padStart(2, "0")} 00:00:00`,
|
`${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}-${String(now.getDate()).padStart(2, "0")} 23:59:59`,
|
];
|
}
|
|
function buildTimeParams() {
|
if (!hourRange.value || hourRange.value.length !== 2) {
|
return { ...getRecentHourRange(24 * 7), ignoreRadio: 1 };
|
}
|
return {
|
startTime: formatMinuteTime(new Date(hourRange.value[0])),
|
endTime: formatMinuteTime(new Date(hourRange.value[1])),
|
ignoreRadio: 1,
|
};
|
}
|
|
async function loadSyncStatus() {
|
const res = await getSyncStatus();
|
syncStatus.value = res.data || {};
|
}
|
|
async function fetchData() {
|
loading.value = true;
|
try {
|
const params = { dimension: "hour", ...buildTimeParams() };
|
const res = await listStatisticEle(params);
|
tableData.value = res.data || [];
|
page.current = 1;
|
} finally {
|
loading.value = false;
|
}
|
}
|
|
async function loadMeters() {
|
meterLoading.value = true;
|
try {
|
const res = await meterListPage({
|
keyword: meterKeyword.value,
|
current: meterPage.current,
|
size: meterPage.size,
|
});
|
meterTableData.value = res.data.records || [];
|
meterPage.total = res.data.total || 0;
|
} finally {
|
meterLoading.value = false;
|
}
|
}
|
|
function handleSelectionChange(rows) {
|
selectedRows.value = rows;
|
}
|
|
function openRecordForm(type, row) {
|
formDiaRef.value.open(type, row);
|
}
|
|
function openMeterForm(type, row) {
|
meterFormDiaRef.value.open(type, row);
|
}
|
|
function handleDelete() {
|
ElMessageBox.confirm("确认删除选中的电量记录?", "提示", { type: "warning" })
|
.then(async () => {
|
await eleRecordDelete(selectedRows.value.map((r) => r.id));
|
ElMessage.success("删除成功");
|
handleRefresh();
|
})
|
.catch(() => {});
|
}
|
|
function handleMeterDelete(row) {
|
ElMessageBox.confirm(`确认删除电表「${row.meterName}」?`, "提示", { type: "warning" })
|
.then(async () => {
|
await meterDelete([row.id]);
|
ElMessage.success("删除成功");
|
loadMeters();
|
})
|
.catch(() => {});
|
}
|
|
async function handleMeterSync() {
|
meterSyncing.value = true;
|
try {
|
const res = await meterSync();
|
ElMessage.success(res.msg || "同步成功");
|
loadMeters();
|
loadSyncStatus();
|
} finally {
|
meterSyncing.value = false;
|
}
|
}
|
|
function handleRefresh() {
|
loadSyncStatus();
|
fetchData();
|
}
|
|
function handlePagination(obj) {
|
page.current = obj.page;
|
page.size = obj.limit;
|
}
|
|
function handleMeterPagination(obj) {
|
meterPage.current = obj.page;
|
meterPage.size = obj.limit;
|
loadMeters();
|
}
|
|
onMounted(() => {
|
initDefaultRange();
|
handleRefresh();
|
loadMeters();
|
});
|
</script>
|
|
<style scoped>
|
.sync-card { margin-bottom: 16px; }
|
.sync-item {
|
background: #f5f7fa;
|
border-radius: 8px;
|
padding: 16px;
|
text-align: center;
|
height: 100%;
|
}
|
.sync-label { font-size: 13px; color: #909399; margin-bottom: 8px; }
|
.sync-value { font-size: 24px; font-weight: 600; color: #303133; }
|
.sync-value.sync-time { font-size: 14px; font-weight: 500; color: #606266; line-height: 1.4; word-break: break-all; }
|
.card-header { display: flex; align-items: center; gap: 12px; }
|
.card-header .desc { font-size: 13px; color: #909399; }
|
.search-form { margin-bottom: 16px; }
|
.stat-row { margin-bottom: 16px; }
|
.stat-item { background: #f5f7fa; border-radius: 8px; padding: 16px; text-align: center; }
|
.stat-label { font-size: 13px; color: #909399; margin-bottom: 8px; }
|
.stat-value { font-size: 24px; font-weight: 600; }
|
.meter-toolbar { display: flex; gap: 10px; margin-bottom: 16px; align-items: center; }
|
</style>
|