From 80ba6fd4314eaf5fbaf73198d1a61a1323fa4f45 Mon Sep 17 00:00:00 2001
From: yuan <123@>
Date: 星期二, 16 六月 2026 09:54:28 +0800
Subject: [PATCH] feat: 添加能耗数据查询和电表管理功能

---
 src/views/energyManagement/meterArchive/index.vue                           |  124 +++
 src/views/energyManagement/energyDataCollection/components/meterFormDia.vue |  114 ++
 src/views/energyManagement/collectorArchive/components/formDia.vue          |   93 ++
 src/views/energyManagement/energyDataCollection/components/formDia.vue      |  210 +++++
 src/views/energyManagement/energyDataCollection/index.vue                   |  385 +++++++++
 src/views/energyManagement/energyRealTimeMonitor/index.vue                  |  336 ++++++++
 src/views/energyManagement/meterArchive/components/formDia.vue              |  114 ++
 src/views/energyManagement/collectorArchive/index.vue                       |   96 ++
 src/api/energyManagement/tqdianbiao.js                                      |   58 +
 src/components/Pagination/index.vue                                         |  208 ++--
 src/api/energyManagement/statisticEle.js                                    |  140 +++
 src/views/energyManagement/energyStatistics/index.vue                       |  426 ++++++++++
 12 files changed, 2,200 insertions(+), 104 deletions(-)

diff --git a/src/api/energyManagement/statisticEle.js b/src/api/energyManagement/statisticEle.js
new file mode 100644
index 0000000..289d98a
--- /dev/null
+++ b/src/api/energyManagement/statisticEle.js
@@ -0,0 +1,140 @@
+import request from "@/utils/request";
+
+/** 鑳借�楁暟鎹垪琛ㄦ煡璇� */
+export function listStatisticEle(query) {
+  return request({
+    url: "/statisticEle/list",
+    method: "get",
+    params: query,
+  });
+}
+
+/** 鑳借�楁眹鎬荤粺璁� */
+export function summaryStatisticEle(query) {
+  return request({
+    url: "/statisticEle/summary",
+    method: "get",
+    params: query,
+  });
+}
+
+/** 鏄ㄦ棩鐢ㄧ數閲忔眹鎬� */
+export function getYesterdaySummary() {
+  return request({
+    url: "/statisticEle/yesterday",
+    method: "get",
+  });
+}
+
+/** 鑾峰彇鏄ㄥぉ鏃ユ湡 YYYYMMDD */
+export function getYesterdayDayTime() {
+  const d = new Date();
+  d.setDate(d.getDate() - 1);
+  return formatDayTime(d);
+}
+
+/** 鑾峰彇鏄ㄥぉ鏃ユ湡 YYYY-MM-DD */
+export function getYesterdayDayPicker() {
+  const d = new Date();
+  d.setDate(d.getDate() - 1);
+  return formatDayPicker(d);
+}
+
+/** 鍚屾鐘舵�� */
+export function getSyncStatus() {
+  return request({
+    url: "/statisticEle/syncStatus",
+    method: "get",
+  });
+}
+
+/** 鏍煎紡鍖栨椂闂翠负灏忔椂缁村害 YYYYMMDDHH */
+export function formatHourTime(date) {
+  const y = date.getFullYear();
+  const m = String(date.getMonth() + 1).padStart(2, "0");
+  const d = String(date.getDate()).padStart(2, "0");
+  const h = String(date.getHours()).padStart(2, "0");
+  return `${y}${m}${d}${h}`;
+}
+
+/** 鏍煎紡鍖栨椂闂翠负澶╃淮搴� YYYYMMDD */
+export function formatDayTime(date) {
+  const y = date.getFullYear();
+  const m = String(date.getMonth() + 1).padStart(2, "0");
+  const d = String(date.getDate()).padStart(2, "0");
+  return `${y}${m}${d}`;
+}
+
+/** 澶╃淮搴﹁浆鏃ユ湡閫夋嫨鍣ㄦ牸寮� YYYY-MM-DD */
+export function formatDayPicker(date) {
+  const y = date.getFullYear();
+  const m = String(date.getMonth() + 1).padStart(2, "0");
+  const d = String(date.getDate()).padStart(2, "0");
+  return `${y}-${m}-${d}`;
+}
+
+/** 鏍煎紡鍖栨椂闂翠负鏈堢淮搴� YYYYMM */
+export function formatMonthTime(date) {
+  const y = date.getFullYear();
+  const m = String(date.getMonth() + 1).padStart(2, "0");
+  return `${y}${m}`;
+}
+
+/** 鏍煎紡鍖栨椂闂翠负骞寸淮搴� YYYY */
+export function formatYearTime(date) {
+  return String(date.getFullYear());
+}
+
+/** 瑙f瀽鏃堕棿鏍囪瘑涓哄彲璇绘牸寮� */
+export function parseTimeKey(timeKey, dimension) {
+  if (!timeKey) return "-";
+  if (dimension === "hour" && timeKey.length >= 10) {
+    return `${timeKey.slice(0, 4)}-${timeKey.slice(4, 6)}-${timeKey.slice(6, 8)} ${timeKey.slice(8, 10)}:00`;
+  }
+  if ((dimension === "manual" || dimension === "minute") && timeKey.length >= 12) {
+    return `${timeKey.slice(0, 4)}-${timeKey.slice(4, 6)}-${timeKey.slice(6, 8)} ${timeKey.slice(8, 10)}:${timeKey.slice(10, 12)}`;
+  }
+  if (dimension === "day" && timeKey.length >= 8) {
+    return `${timeKey.slice(0, 4)}-${timeKey.slice(4, 6)}-${timeKey.slice(6, 8)}`;
+  }
+  if (dimension === "month" && timeKey.length >= 6) {
+    return `${timeKey.slice(0, 4)}-${timeKey.slice(4, 6)}`;
+  }
+  if (dimension === "quarter" && timeKey.includes("Q")) {
+    const [y, q] = timeKey.split("Q");
+    return `${y}骞� 绗�${q}瀛e害`;
+  }
+  if (dimension === "year" && timeKey.length >= 4) {
+    return `${timeKey.slice(0, 4)}骞碻;
+  }
+  return timeKey;
+}
+
+/** 鏍煎紡鍖栨椂闂翠负鍒嗛挓缁村害 YYYYMMDDHHmm */
+export function formatMinuteTime(date) {
+  const y = date.getFullYear();
+  const m = String(date.getMonth() + 1).padStart(2, "0");
+  const d = String(date.getDate()).padStart(2, "0");
+  const h = String(date.getHours()).padStart(2, "0");
+  const min = String(date.getMinutes()).padStart(2, "0");
+  return `${y}${m}${d}${h}${min}`;
+}
+
+/** 鏃堕棿鑼冨洿杞煡璇� key锛堟敮鎸佸垎閽熺簿搴︼級 */
+export function formatRangeStart(date) {
+  return formatMinuteTime(date);
+}
+
+export function formatRangeEnd(date) {
+  return formatMinuteTime(date);
+}
+
+/** 鑾峰彇鏈�杩� N 灏忔椂鐨勬椂闂磋寖鍥� */
+export function getRecentHourRange(hours = 24) {
+  const end = new Date();
+  const start = new Date(end.getTime() - hours * 3600000);
+  return {
+    startTime: formatMinuteTime(start),
+    endTime: formatMinuteTime(end),
+  };
+}
diff --git a/src/api/energyManagement/tqdianbiao.js b/src/api/energyManagement/tqdianbiao.js
new file mode 100644
index 0000000..011bbba
--- /dev/null
+++ b/src/api/energyManagement/tqdianbiao.js
@@ -0,0 +1,58 @@
+import request from "@/utils/request";
+
+// ========== 閲囬泦鍣ㄦ。妗� ==========
+export function collectorListPage(query) {
+  return request({ url: "/tqdianbiao/collector/listPage", method: "get", params: query });
+}
+export function collectorListAll() {
+  return request({ url: "/tqdianbiao/collector/listAll", method: "get" });
+}
+export function collectorAdd(data) {
+  return request({ url: "/tqdianbiao/collector/add", method: "post", data });
+}
+export function collectorUpdate(data) {
+  return request({ url: "/tqdianbiao/collector/update", method: "post", data });
+}
+export function collectorDelete(ids) {
+  return request({ url: "/tqdianbiao/collector/delete", method: "delete", data: ids });
+}
+export function collectorSync() {
+  return request({ url: "/tqdianbiao/collector/sync", method: "post" });
+}
+
+// ========== 鐢佃〃妗f ==========
+export function meterListPage(query) {
+  return request({ url: "/tqdianbiao/meter/listPage", method: "get", params: query });
+}
+export function meterListAll() {
+  return request({ url: "/tqdianbiao/meter/listAll", method: "get" });
+}
+export function meterAdd(data) {
+  return request({ url: "/tqdianbiao/meter/add", method: "post", data });
+}
+export function meterUpdate(data) {
+  return request({ url: "/tqdianbiao/meter/update", method: "post", data });
+}
+export function meterDelete(ids) {
+  return request({ url: "/tqdianbiao/meter/delete", method: "delete", data: ids });
+}
+export function meterSync() {
+  return request({ url: "/tqdianbiao/meter/sync", method: "post" });
+}
+
+// ========== 鐢甸噺璁板綍(灏忔椂/澶╂墜鍔ㄥ綍鍏�) ==========
+export function eleRecordListPage(query) {
+  return request({ url: "/tqdianbiao/eleRecord/listPage", method: "get", params: query });
+}
+export function eleRecordAdd(data) {
+  return request({ url: "/tqdianbiao/eleRecord/add", method: "post", data });
+}
+export function eleRecordUpdate(data) {
+  return request({ url: "/tqdianbiao/eleRecord/update", method: "post", data });
+}
+export function eleRecordDelete(ids) {
+  return request({ url: "/tqdianbiao/eleRecord/delete", method: "delete", data: ids });
+}
+export function eleRecordPrevReading(params) {
+  return request({ url: "/tqdianbiao/eleRecord/prevReading", method: "get", params });
+}
diff --git a/src/components/Pagination/index.vue b/src/components/Pagination/index.vue
index 53fbec2..656b061 100644
--- a/src/components/Pagination/index.vue
+++ b/src/components/Pagination/index.vue
@@ -1,105 +1,105 @@
-<template>
-  <div :class="{ 'hidden': hidden }" class="pagination-container">
-    <el-pagination
-      :background="background"
-      v-model:current-page="currentPage"
-      v-model:page-size="pageSize"
-      :layout="layout"
-      :page-sizes="pageSizes"
-      :pager-count="pagerCount"
-      :total="total"
-      @size-change="handleSizeChange"
-      @current-change="handleCurrentChange"
-    />
-  </div>
-</template>
-
-<script setup>
-import { scrollTo } from '@/utils/scroll-to'
-
-const props = defineProps({
-  total: {
-    required: true,
-    type: Number
-  },
-  page: {
-    type: Number,
-    default: 1
-  },
-  limit: {
-    type: Number,
-    default: 20
-  },
-  pageSizes: {
-    type: Array,
-    default() {
-      return [10, 20, 30, 50]
-    }
-  },
-  // 绉诲姩绔〉鐮佹寜閽殑鏁伴噺绔粯璁ゅ��5
-  pagerCount: {
-    type: Number,
-    default: document.body.clientWidth < 992 ? 5 : 7
-  },
-  layout: {
-    type: String,
-    default: 'total, sizes, prev, pager, next, jumper'
-  },
-  background: {
-    type: Boolean,
-    default: true
-  },
-  autoScroll: {
-    type: Boolean,
-    default: true
-  },
-  hidden: {
-    type: Boolean,
-    default: false
-  }
-})
-
-const emit = defineEmits()
-const currentPage = computed({
-  get() {
-    return props.page
-  },
-  set(val) {
-    emit('update:page', val)
-  }
-})
-const pageSize = computed({
-  get() {
-    return props.limit
-  },
-  set(val){
-    emit('update:limit', val)
-  }
-})
-
-function handleSizeChange(val) {
-  if (currentPage.value * val > props.total) {
-    currentPage.value = 1
-  }
-  emit('pagination', { page: currentPage.value, limit: val })
-  if (props.autoScroll) {
-    scrollTo(0, 800)
-  }
-}
-
-function handleCurrentChange(val) {
-  emit('pagination', { page: val, limit: pageSize.value })
-  if (props.autoScroll) {
-    scrollTo(0, 800)
-  }
-}
-</script>
-
-<style scoped>
-.pagination-container {
-  background: #fff;
-}
-.pagination-container.hidden {
-  display: none;
-}
+<template>
+  <div :class="{ 'hidden': hidden }" class="pagination-container">
+    <el-pagination
+      :background="background"
+      v-model:current-page="currentPage"
+      v-model:page-size="pageSize"
+      :layout="layout"
+      :page-sizes="pageSizes"
+      :pager-count="pagerCount"
+      :total="total"
+      @size-change="handleSizeChange"
+      @current-change="handleCurrentChange"
+    />
+  </div>
+</template>
+
+<script setup>
+import { scrollTo } from '@/utils/scroll-to'
+
+const props = defineProps({
+  total: {
+    required: true,
+    type: Number
+  },
+  page: {
+    type: Number,
+    default: 1
+  },
+  limit: {
+    type: Number,
+    default: 20
+  },
+  pageSizes: {
+    type: Array,
+    default() {
+      return [10, 20, 30, 50, 100, 200, 500]
+    }
+  },
+  // 绉诲姩绔〉鐮佹寜閽殑鏁伴噺绔粯璁ゅ��5
+  pagerCount: {
+    type: Number,
+    default: document.body.clientWidth < 992 ? 5 : 7
+  },
+  layout: {
+    type: String,
+    default: 'total, sizes, prev, pager, next, jumper'
+  },
+  background: {
+    type: Boolean,
+    default: true
+  },
+  autoScroll: {
+    type: Boolean,
+    default: true
+  },
+  hidden: {
+    type: Boolean,
+    default: false
+  }
+})
+
+const emit = defineEmits()
+const currentPage = computed({
+  get() {
+    return props.page
+  },
+  set(val) {
+    emit('update:page', val)
+  }
+})
+const pageSize = computed({
+  get() {
+    return props.limit
+  },
+  set(val){
+    emit('update:limit', val)
+  }
+})
+
+function handleSizeChange(val) {
+  if (currentPage.value * val > props.total) {
+    currentPage.value = 1
+  }
+  emit('pagination', { page: currentPage.value, limit: val })
+  if (props.autoScroll) {
+    scrollTo(0, 800)
+  }
+}
+
+function handleCurrentChange(val) {
+  emit('pagination', { page: val, limit: pageSize.value })
+  if (props.autoScroll) {
+    scrollTo(0, 800)
+  }
+}
+</script>
+
+<style scoped>
+.pagination-container {
+  background: #fff;
+}
+.pagination-container.hidden {
+  display: none;
+}
 </style>
\ No newline at end of file
diff --git a/src/views/energyManagement/collectorArchive/components/formDia.vue b/src/views/energyManagement/collectorArchive/components/formDia.vue
new file mode 100644
index 0000000..0fc196c
--- /dev/null
+++ b/src/views/energyManagement/collectorArchive/components/formDia.vue
@@ -0,0 +1,93 @@
+<template>
+  <el-dialog v-model="visible" :title="title" width="600px" @close="closeDia">
+    <el-form ref="formRef" :model="form" :rules="rules" label-width="120px">
+      <el-form-item label="閲囬泦鍣ㄦ。妗圛D" prop="collectorId">
+        <el-input v-model="form.collectorId" placeholder="骞冲彴 cid" :disabled="operationType === 'edit'" />
+      </el-form-item>
+      <el-form-item label="閲囬泦鍣ㄥ彿" prop="collectorNo">
+        <el-input v-model="form.collectorNo" placeholder="閲囬泦鍣ㄥ彿" />
+      </el-form-item>
+      <el-form-item label="鍦ㄧ嚎鐘舵��" prop="online">
+        <el-switch v-model="form.online" active-text="鍦ㄧ嚎" inactive-text="绂荤嚎" />
+      </el-form-item>
+      <el-form-item label="淇″彿鍊�" prop="csq">
+        <el-input-number v-model="form.csq" :min="0" :max="31" style="width: 100%" />
+      </el-form-item>
+      <el-form-item label="涓婄嚎鏃堕棿" prop="connectTime">
+        <el-input v-model="form.connectTime" placeholder="濡� 2026-06-15 10:00:00" />
+      </el-form-item>
+      <el-form-item label="鎺夌嚎鏃堕棿" prop="disconnectTime">
+        <el-input v-model="form.disconnectTime" placeholder="濡� 2026-06-15 10:00:00" />
+      </el-form-item>
+      <el-form-item label="澶囨敞" prop="description">
+        <el-input v-model="form.description" type="textarea" :rows="2" />
+      </el-form-item>
+    </el-form>
+    <template #footer>
+      <el-button @click="visible = false">鍙栨秷</el-button>
+      <el-button type="primary" :loading="submitting" @click="submit">纭畾</el-button>
+    </template>
+  </el-dialog>
+</template>
+
+<script setup>
+import { reactive, ref } from "vue";
+import { ElMessage } from "element-plus";
+import { collectorAdd, collectorUpdate } from "@/api/energyManagement/tqdianbiao.js";
+
+const emit = defineEmits(["close"]);
+const visible = ref(false);
+const submitting = ref(false);
+const operationType = ref("add");
+const title = ref("");
+const formRef = ref(null);
+
+const defaultForm = () => ({
+  id: null,
+  collectorId: "",
+  collectorNo: "",
+  online: true,
+  csq: null,
+  connectTime: "",
+  disconnectTime: "",
+  description: "",
+});
+
+const form = reactive(defaultForm());
+
+const rules = {
+  collectorId: [{ required: true, message: "璇疯緭鍏ラ噰闆嗗櫒妗fID", trigger: "blur" }],
+  collectorNo: [{ required: true, message: "璇疯緭鍏ラ噰闆嗗櫒鍙�", trigger: "blur" }],
+};
+
+function open(type, row) {
+  operationType.value = type;
+  title.value = type === "add" ? "鏂板閲囬泦鍣�" : "缂栬緫閲囬泦鍣�";
+  Object.assign(form, defaultForm(), type === "edit" ? { ...row } : {});
+  visible.value = true;
+}
+
+function closeDia() {
+  emit("close");
+}
+
+async function submit() {
+  await formRef.value.validate();
+  submitting.value = true;
+  try {
+    if (operationType.value === "add") {
+      await collectorAdd(form);
+      ElMessage.success("鏂板鎴愬姛");
+    } else {
+      await collectorUpdate(form);
+      ElMessage.success("淇敼鎴愬姛");
+    }
+    visible.value = false;
+    emit("close");
+  } finally {
+    submitting.value = false;
+  }
+}
+
+defineExpose({ open });
+</script>
diff --git a/src/views/energyManagement/collectorArchive/index.vue b/src/views/energyManagement/collectorArchive/index.vue
new file mode 100644
index 0000000..499737f
--- /dev/null
+++ b/src/views/energyManagement/collectorArchive/index.vue
@@ -0,0 +1,96 @@
+<template>
+  <div class="app-container">
+    <div class="search_form">
+      <div>
+        <span class="search_title">鍏抽敭璇嶏細</span>
+        <el-input
+          v-model="searchForm.keyword"
+          style="width: 240px"
+          placeholder="閲囬泦鍣ㄥ彿/妗fID/澶囨敞"
+          clearable
+          @keyup.enter="handleQuery"
+        />
+        <el-button type="primary" @click="handleQuery" style="margin-left: 10px">鎼滅储</el-button>
+      </div>
+      <div>
+        <el-button type="success" :loading="syncing" @click="handleSync">鍚屾閲囬泦鍣�</el-button>
+      </div>
+    </div>
+    <div class="table_list">
+      <PIMTable
+        rowKey="id"
+        :column="tableColumn"
+        :tableData="tableData"
+        :page="page"
+        :tableLoading="tableLoading"
+        @pagination="pagination"
+      >
+        <template #online="{ row }">
+          <el-tag :type="row.online ? 'success' : 'danger'" size="small">
+            {{ row.online ? "鍦ㄧ嚎" : "绂荤嚎" }}
+          </el-tag>
+        </template>
+      </PIMTable>
+    </div>
+  </div>
+</template>
+
+<script setup>
+import { onMounted, reactive, ref, toRefs } from "vue";
+import { ElMessage } from "element-plus";
+import { collectorListPage, collectorSync } from "@/api/energyManagement/tqdianbiao.js";
+
+const tableLoading = ref(false);
+const syncing = ref(false);
+const tableData = ref([]);
+
+const data = reactive({ searchForm: { keyword: "" } });
+const { searchForm } = toRefs(data);
+const page = reactive({ current: 1, size: 10, total: 0 });
+
+const tableColumn = ref([
+  { label: "閲囬泦鍣ㄦ。妗圛D", prop: "collectorId", width: 130 },
+  { label: "閲囬泦鍣ㄥ彿", prop: "collectorNo", width: 140 },
+  { label: "鍦ㄧ嚎鐘舵��", prop: "online", dataType: "slot", slot: "online", width: 100 },
+  { label: "淇″彿鍊�", prop: "csq", width: 80 },
+  { label: "涓婄嚎鏃堕棿", prop: "connectTime", minWidth: 160 },
+  { label: "鎺夌嚎鏃堕棿", prop: "disconnectTime", minWidth: 160 },
+  { label: "澶囨敞", prop: "description", minWidth: 120 },
+  { label: "鍚屾鏃堕棿", prop: "syncTime", minWidth: 160 },
+]);
+
+function handleQuery() {
+  page.current = 1;
+  getList();
+}
+
+function pagination(obj) {
+  page.current = obj.page;
+  page.size = obj.limit;
+  getList();
+}
+
+async function getList() {
+  tableLoading.value = true;
+  try {
+    const res = await collectorListPage({ ...searchForm.value, current: page.current, size: page.size });
+    tableData.value = res.data.records;
+    page.total = res.data.total;
+  } finally {
+    tableLoading.value = false;
+  }
+}
+
+async function handleSync() {
+  syncing.value = true;
+  try {
+    const res = await collectorSync();
+    ElMessage.success(res.msg || "鍚屾鎴愬姛");
+    getList();
+  } finally {
+    syncing.value = false;
+  }
+}
+
+onMounted(() => getList());
+</script>
diff --git a/src/views/energyManagement/energyDataCollection/components/formDia.vue b/src/views/energyManagement/energyDataCollection/components/formDia.vue
new file mode 100644
index 0000000..a231b7e
--- /dev/null
+++ b/src/views/energyManagement/energyDataCollection/components/formDia.vue
@@ -0,0 +1,210 @@
+<template>
+  <el-dialog v-model="visible" :title="title" width="560px" @close="closeDia">
+    <el-form ref="formRef" :model="form" :rules="rules" label-width="110px">
+      <el-form-item label="鐢佃〃" prop="meterId">
+        <el-select
+          v-model="form.meterId"
+          placeholder="璇烽�夋嫨鐢佃〃"
+          filterable
+          style="width: 100%"
+          :disabled="operationType === 'edit'"
+          @change="handleMeterChange"
+        >
+          <el-option
+            v-for="item in meterList"
+            :key="item.meterId"
+            :label="`${item.meterName || item.address} (ID:${item.meterId})`"
+            :value="item.meterId"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="缁熻鏃堕棿" prop="recordTime">
+        <el-date-picker
+          v-model="form.recordTime"
+          type="datetime"
+          placeholder="绮剧‘鍒板垎閽�"
+          format="YYYY-MM-DD HH:mm"
+          value-format="YYYY-MM-DD HH:mm:00"
+          style="width: 100%"
+          @change="handleTimeChange"
+        />
+      </el-form-item>
+      <el-form-item label="鍊嶇巼">
+        <el-input-number v-model="form.ratio" :min="1" style="width: 100%" @change="calcConsumption" />
+      </el-form-item>
+      <el-form-item label="涓婃鐢甸噺" prop="prevReading">
+        <el-input-number
+          v-model="form.prevReading"
+          :min="0"
+          :precision="4"
+          style="width: 100%"
+          @change="calcConsumption"
+        />
+      </el-form-item>
+      <el-form-item label="鏈鐢甸噺" prop="currReading">
+        <el-input-number
+          v-model="form.currReading"
+          :min="0"
+          :precision="4"
+          style="width: 100%"
+          @change="calcConsumption"
+        />
+      </el-form-item>
+      <el-form-item label="鏈鐢ㄧ數閲�">
+        <el-input :model-value="consumptionDisplay" disabled>
+          <template #suffix>kWh</template>
+        </el-input>
+      </el-form-item>
+    </el-form>
+    <template #footer>
+      <el-button @click="visible = false">鍙栨秷</el-button>
+      <el-button type="primary" :loading="submitting" @click="submit">纭畾</el-button>
+    </template>
+  </el-dialog>
+</template>
+
+<script setup>
+import { computed, reactive, ref } from "vue";
+import { ElMessage } from "element-plus";
+import { eleRecordAdd, eleRecordUpdate, meterListAll, eleRecordPrevReading } from "@/api/energyManagement/tqdianbiao.js";
+import { formatMinuteTime } from "@/api/energyManagement/statisticEle.js";
+
+const emit = defineEmits(["close"]);
+const visible = ref(false);
+const submitting = ref(false);
+const operationType = ref("add");
+const title = ref("");
+const formRef = ref(null);
+const meterList = ref([]);
+
+const defaultForm = () => ({
+  id: null,
+  dimension: "manual",
+  meterId: null,
+  recordTime: "",
+  prevReading: null,
+  currReading: null,
+  totalConsumption: null,
+  ratio: 1,
+});
+
+const form = reactive(defaultForm());
+
+const rules = {
+  meterId: [{ required: true, message: "璇烽�夋嫨鐢佃〃", trigger: "change" }],
+  recordTime: [{ required: true, message: "璇烽�夋嫨鏃堕棿", trigger: "change" }],
+  prevReading: [{ required: true, message: "璇疯緭鍏ヤ笂娆$數閲�", trigger: "blur" }],
+  currReading: [{ required: true, message: "璇疯緭鍏ユ湰娆$數閲�", trigger: "blur" }],
+};
+
+const consumptionDisplay = computed(() => {
+  if (form.totalConsumption == null) return "-";
+  return Number(form.totalConsumption).toFixed(4);
+});
+
+async function loadMeters() {
+  const res = await meterListAll();
+  meterList.value = res.data || [];
+}
+
+function recordTimeFromRow(row) {
+  if (row.timeKey?.length >= 12) {
+    const k = row.timeKey;
+    return `${k.slice(0, 4)}-${k.slice(4, 6)}-${k.slice(6, 8)} ${k.slice(8, 10)}:${k.slice(10, 12)}:00`;
+  }
+  if (row.timeKey?.length >= 10) {
+    const k = row.timeKey;
+    return `${k.slice(0, 4)}-${k.slice(4, 6)}-${k.slice(6, 8)} ${k.slice(8, 10)}:00:00`;
+  }
+  return "";
+}
+
+function calcConsumption() {
+  if (form.prevReading == null || form.currReading == null || form.ratio == null) {
+    form.totalConsumption = null;
+    return;
+  }
+  const diff = form.currReading - form.prevReading;
+  form.totalConsumption = Number((diff * form.ratio).toFixed(4));
+}
+
+function handleMeterChange(meterId) {
+  const meter = meterList.value.find((m) => m.meterId === meterId);
+  if (meter?.rate) {
+    form.ratio = meter.rate;
+  }
+  calcConsumption();
+  if (form.recordTime) {
+    loadPrevReading();
+  }
+}
+
+function handleTimeChange() {
+  loadPrevReading();
+}
+
+async function loadPrevReading() {
+  if (!form.meterId || !form.recordTime) return;
+  const timeKey = formatMinuteTime(new Date(form.recordTime));
+  const res = await eleRecordPrevReading({ meterId: form.meterId, timeKey });
+  if (res.data != null) {
+    form.prevReading = Number(res.data);
+    calcConsumption();
+  }
+}
+
+function open(type, row) {
+  operationType.value = type;
+  title.value = type === "add" ? "鎵嬪姩鎶勮〃褰曞叆" : "缂栬緫鎶勮〃璁板綍";
+  Object.assign(form, defaultForm());
+  if (type === "edit" && row) {
+    Object.assign(form, {
+      id: row.id,
+      meterId: row.meterId,
+      recordTime: recordTimeFromRow(row),
+      prevReading: row.prevReading != null ? Number(row.prevReading) : null,
+      currReading: row.currReading != null ? Number(row.currReading) : null,
+      totalConsumption: row.totalConsumption != null ? Number(row.totalConsumption) : null,
+      ratio: row.ratio || 1,
+    });
+  }
+  loadMeters();
+  visible.value = true;
+}
+
+function closeDia() {
+  emit("close");
+}
+
+async function submit() {
+  await formRef.value.validate();
+  calcConsumption();
+  submitting.value = true;
+  try {
+    const payload = {
+      id: form.id,
+      dimension: "manual",
+      meterId: form.meterId,
+      timeKey: formatMinuteTime(new Date(form.recordTime)),
+      prevReading: form.prevReading,
+      currReading: form.currReading,
+      totalConsumption: form.totalConsumption,
+      ratio: form.ratio,
+      readingMethod: "manual",
+    };
+    if (operationType.value === "add") {
+      await eleRecordAdd(payload);
+      ElMessage.success("褰曞叆鎴愬姛");
+    } else {
+      await eleRecordUpdate(payload);
+      ElMessage.success("淇敼鎴愬姛");
+    }
+    visible.value = false;
+    emit("close");
+  } finally {
+    submitting.value = false;
+  }
+}
+
+defineExpose({ open });
+</script>
diff --git a/src/views/energyManagement/energyDataCollection/components/meterFormDia.vue b/src/views/energyManagement/energyDataCollection/components/meterFormDia.vue
new file mode 100644
index 0000000..c2f77ec
--- /dev/null
+++ b/src/views/energyManagement/energyDataCollection/components/meterFormDia.vue
@@ -0,0 +1,114 @@
+<template>
+  <el-dialog v-model="visible" :title="title" width="520px" @close="closeDia">
+    <el-form ref="formRef" :model="form" :rules="rules" label-width="100px">
+      <el-form-item label="鐢佃〃鍚嶇О" prop="meterName">
+        <el-input v-model="form.meterName" placeholder="璇疯緭鍏ョ數琛ㄥ悕绉�" />
+      </el-form-item>
+      <el-form-item label="琛ㄥ湴鍧�" prop="address">
+        <el-input v-model="form.address" placeholder="璇疯緭鍏ヨ〃鍦板潃" />
+      </el-form-item>
+      <el-form-item label="澶囨敞">
+        <el-input v-model="form.description" type="textarea" :rows="2" placeholder="澶囨敞" />
+      </el-form-item>
+      <el-form-item label="缁х數鍣ㄧ姸鎬�" prop="relayState">
+        <el-select v-model="form.relayState" style="width: 100%">
+          <el-option label="鍚堥椄" value="1" />
+          <el-option label="鎷夐椄" value="0" />
+        </el-select>
+      </el-form-item>
+      <el-form-item v-if="showRate" label="鍊嶇巼">
+        <el-input-number v-model="form.rate" :min="1" style="width: 100%" />
+      </el-form-item>
+      <el-form-item v-if="operationType === 'edit' && form.source === 'sync'" label="妗fID">
+        <el-input :model-value="form.meterId" disabled />
+      </el-form-item>
+      <el-form-item v-if="operationType === 'edit' && form.source === 'sync'">
+        <el-text type="info" size="small">鍚屾鐢佃〃浠呭彲淇敼鍚嶇О銆佽〃鍦板潃銆佸娉ㄣ�佺户鐢靛櫒鐘舵�侊紙涓嶅悓姝ュ埌鑳芥簮骞冲彴锛�</el-text>
+      </el-form-item>
+    </el-form>
+    <template #footer>
+      <el-button @click="visible = false">鍙栨秷</el-button>
+      <el-button type="primary" :loading="submitting" @click="submit">纭畾</el-button>
+    </template>
+  </el-dialog>
+</template>
+
+<script setup>
+import { computed, reactive, ref } from "vue";
+import { ElMessage } from "element-plus";
+import { meterAdd, meterUpdate } from "@/api/energyManagement/tqdianbiao.js";
+
+const emit = defineEmits(["close"]);
+const visible = ref(false);
+const submitting = ref(false);
+const operationType = ref("add");
+const title = ref("");
+const formRef = ref(null);
+
+const defaultForm = () => ({
+  id: null,
+  meterId: null,
+  meterName: "",
+  address: "",
+  description: "",
+  relayState: "1",
+  rate: 1,
+  source: "manual",
+});
+
+const form = reactive(defaultForm());
+
+const showRate = computed(() => operationType.value === "edit" && form.source === "manual");
+
+const rules = {
+  address: [{ required: true, message: "璇疯緭鍏ヨ〃鍦板潃", trigger: "blur" }],
+  relayState: [{ required: true, message: "璇烽�夋嫨缁х數鍣ㄧ姸鎬�", trigger: "change" }],
+};
+
+function open(type, row) {
+  operationType.value = type;
+  title.value = type === "add" ? "鏂板鐢佃〃" : "缂栬緫鐢佃〃";
+  Object.assign(form, defaultForm());
+  if (type === "edit" && row) {
+    Object.assign(form, {
+      id: row.id,
+      meterId: row.meterId,
+      meterName: row.meterName || row.address || "",
+      address: row.address || "",
+      description: row.description || "",
+      relayState: row.relayState || "1",
+      rate: row.rate || 1,
+      source: row.source || "sync",
+    });
+  }
+  visible.value = true;
+}
+
+function closeDia() {
+  emit("close");
+}
+
+async function submit() {
+  await formRef.value.validate();
+  submitting.value = true;
+  try {
+    const payload = { ...form };
+    if (!payload.meterName) {
+      payload.meterName = payload.address;
+    }
+    if (operationType.value === "add") {
+      await meterAdd(payload);
+      ElMessage.success("鏂板鎴愬姛");
+    } else {
+      await meterUpdate(payload);
+      ElMessage.success("淇敼鎴愬姛");
+    }
+    visible.value = false;
+    emit("close");
+  } finally {
+    submitting.value = false;
+  }
+}
+
+defineExpose({ open });
+</script>
diff --git a/src/views/energyManagement/energyDataCollection/index.vue b/src/views/energyManagement/energyDataCollection/index.vue
new file mode 100644
index 0000000..36b6b6c
--- /dev/null
+++ b/src/views/energyManagement/energyDataCollection/index.vue
@@ -0,0 +1,385 @@
+<template>
+  <div class="app-container">
+    <el-card class="sync-card">
+      <el-row :gutter="16">
+        <el-col :span="6">
+          <div class="sync-item">
+            <div class="sync-label">鐢佃〃鏁伴噺</div>
+            <div class="sync-value">{{ syncStatus.meterCount ?? 0 }}</div>
+          </div>
+        </el-col>
+        <el-col :span="6">
+          <div class="sync-item">
+            <div class="sync-label">閲囬泦鍣ㄥ湪绾�</div>
+            <div class="sync-value online">{{ syncStatus.onlineCollectorCount ?? 0 }} / {{ syncStatus.collectorCount ?? 0 }}</div>
+          </div>
+        </el-col>
+        <el-col :span="6">
+          <div class="sync-item">
+            <div class="sync-label">灏忔椂鏁版嵁鍚屾</div>
+            <div class="sync-value small">{{ lastHourSync }}</div>
+          </div>
+        </el-col>
+        <el-col :span="6">
+          <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">鍚屾灏忔椂鐢甸噺 + 鎵嬪姩鎶勮〃锛坕gnore_radio=1锛�</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="妗f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 { text-align: center; padding: 8px 0; }
+.sync-label { font-size: 12px; color: #909399; margin-bottom: 6px; }
+.sync-value { font-size: 20px; font-weight: 600; }
+.sync-value.online { color: #67c23a; }
+.sync-value.small { font-size: 13px; font-weight: 500; }
+.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>
diff --git a/src/views/energyManagement/energyRealTimeMonitor/index.vue b/src/views/energyManagement/energyRealTimeMonitor/index.vue
new file mode 100644
index 0000000..426e2c7
--- /dev/null
+++ b/src/views/energyManagement/energyRealTimeMonitor/index.vue
@@ -0,0 +1,336 @@
+<template>
+  <div class="app-container">
+    <div class="page-header">
+      <div>
+        <h2>鑳借�楀疄鏃剁洃鎺�</h2>
+        <p class="subtitle">灞曠ず鏈湴宸插悓姝ョ殑鐢ㄧ數鏁版嵁锛堝畾鏃朵换鍔℃瘡灏忔椂鍚屾锛�</p>
+      </div>
+      <div class="header-actions">
+        <el-tag :type="autoRefresh ? 'success' : 'info'" size="small">
+          {{ autoRefresh ? "鑷姩鍒锋柊涓�" : "宸叉殏鍋�" }}
+        </el-tag>
+        <span class="update-time">鏇存柊锛歿{ lastUpdateTime }}</span>
+        <el-switch v-model="autoRefresh" active-text="鑷姩鍒锋柊" @change="toggleAutoRefresh" />
+        <el-button type="primary" :loading="loading" @click="loadData">
+          <el-icon><Refresh /></el-icon>
+          绔嬪嵆鍒锋柊
+        </el-button>
+      </div>
+    </div>
+
+    <el-card class="yesterday-banner" v-loading="yesterdayLoading" shadow="hover">
+      <div class="yesterday-row">
+        <div>
+          <div class="yesterday-title">鏄ㄦ棩鎬荤敤鐢碉紙{{ getYesterdayDayPicker() }}锛�</div>
+          <div class="yesterday-value">{{ yesterdaySummary.totalConsumption ?? 0 }} <span>kWh</span></div>
+        </div>
+        <div class="yesterday-stats">
+          <span>骞冲潎 {{ yesterdaySummary.avgConsumption ?? 0 }} kWh</span>
+          <span>鏈�澶� {{ yesterdaySummary.maxConsumption ?? 0 }} kWh</span>
+          <span>鏈�灏� {{ yesterdaySummary.minConsumption ?? 0 }} kWh</span>
+        </div>
+      </div>
+    </el-card>
+
+    <el-row :gutter="16" class="monitor-cards">
+      <el-col :span="8">
+        <el-card shadow="hover">
+          <div class="monitor-card">
+            <div class="monitor-title">褰撳墠灏忔椂鐢ㄧ數</div>
+            <div class="monitor-value">
+              {{ currentHourConsumption }}
+              <span class="unit">kWh</span>
+            </div>
+          </div>
+        </el-card>
+      </el-col>
+      <el-col :span="8">
+        <el-card shadow="hover">
+          <div class="monitor-card">
+            <div class="monitor-title">杩�24灏忔椂绱</div>
+            <div class="monitor-value">
+              {{ totalConsumption }}
+              <span class="unit">kWh</span>
+            </div>
+          </div>
+        </el-card>
+      </el-col>
+      <el-col :span="8">
+        <el-card shadow="hover">
+          <div class="monitor-card">
+            <div class="monitor-title">骞冲潎灏忔椂鐢ㄧ數</div>
+            <div class="monitor-value">
+              {{ avgConsumption }}
+              <span class="unit">kWh</span>
+            </div>
+          </div>
+        </el-card>
+      </el-col>
+    </el-row>
+
+    <el-card class="chart-card">
+      <template #header>
+        <div class="card-header">
+          <span>杩�24灏忔椂鐢ㄧ數瓒嬪娍</span>
+          <el-radio-group v-model="chartType" size="small" @change="renderChart">
+            <el-radio-button value="line">鎶樼嚎鍥�</el-radio-button>
+            <el-radio-button value="bar">鏌辩姸鍥�</el-radio-button>
+          </el-radio-group>
+        </div>
+      </template>
+      <div ref="chartRef" class="chart-container"></div>
+    </el-card>
+
+    <el-card class="table-card">
+      <template #header>
+        <span>瀹炴椂閲囬泦鏄庣粏</span>
+      </template>
+      <el-table v-loading="loading" :data="records" border stripe max-height="320">
+        <el-table-column label="鏃堕棿" min-width="160">
+          <template #default="{ row }">
+            {{ parseTimeKey(row.timeKey, "hour") }}
+          </template>
+        </el-table-column>
+        <el-table-column prop="meterId" label="鐢佃〃ID" width="100" />
+        <el-table-column prop="totalConsumption" label="鐢ㄧ數閲�(kWh)" width="120" />
+        <el-table-column prop="startTime" label="寮�濮嬫椂闂�" min-width="160" />
+        <el-table-column prop="endTime" label="缁撴潫鏃堕棿" min-width="160" />
+        <el-table-column prop="endReading" label="褰撳墠璇绘暟" min-width="160" show-overflow-tooltip />
+      </el-table>
+    </el-card>
+  </div>
+</template>
+
+<script setup>
+import { computed, onBeforeUnmount, onMounted, ref } from "vue";
+import { Refresh } from "@element-plus/icons-vue";
+import * as echarts from "echarts";
+import {
+  summaryStatisticEle,
+  getYesterdaySummary,
+  getYesterdayDayPicker,
+  parseTimeKey,
+  getRecentHourRange,
+} from "@/api/energyManagement/statisticEle.js";
+
+const loading = ref(false);
+const yesterdayLoading = ref(false);
+const yesterdaySummary = ref({});
+const autoRefresh = ref(true);
+const lastUpdateTime = ref("-");
+const chartType = ref("line");
+const records = ref([]);
+const chartRecords = ref([]);
+const chartRef = ref(null);
+let chartInstance = null;
+let refreshTimer = null;
+
+const totalConsumption = computed(() => {
+  const total = chartRecords.value.reduce(
+    (sum, item) => sum + (item.totalConsumption || 0),
+    0
+  );
+  return total.toFixed(2);
+});
+
+const avgConsumption = computed(() => {
+  if (!chartRecords.value.length) return "0.00";
+  return (Number(totalConsumption.value) / chartRecords.value.length).toFixed(2);
+});
+
+const currentHourConsumption = computed(() => {
+  if (!chartRecords.value.length) return "0.00";
+  const latest = chartRecords.value[chartRecords.value.length - 1];
+  return (latest.totalConsumption || 0).toFixed(2);
+});
+
+async function loadYesterday() {
+  yesterdayLoading.value = true;
+  try {
+    const res = await getYesterdaySummary();
+    yesterdaySummary.value = res.data || {};
+  } finally {
+    yesterdayLoading.value = false;
+  }
+}
+
+async function loadData() {
+  loading.value = true;
+  try {
+    const { startTime, endTime } = getRecentHourRange(24);
+    const res = await summaryStatisticEle({
+      dimension: "hour",
+      startTime,
+      endTime,
+    });
+    records.value = res.data?.records || [];
+    chartRecords.value = res.data?.chartRecords || [];
+    lastUpdateTime.value = new Date().toLocaleString();
+    renderChart();
+  } finally {
+    loading.value = false;
+  }
+}
+
+function renderChart() {
+  if (!chartRef.value) return;
+  if (!chartInstance) {
+    chartInstance = echarts.init(chartRef.value);
+  }
+  const labels = chartRecords.value.map((item) => parseTimeKey(item.timeKey, "hour"));
+  const values = chartRecords.value.map((item) => item.totalConsumption || 0);
+  chartInstance.setOption({
+    tooltip: { trigger: "axis" },
+    grid: { left: 50, right: 30, top: 30, bottom: 60 },
+    xAxis: {
+      type: "category",
+      data: labels,
+      axisLabel: { rotate: 35, fontSize: 11 },
+    },
+    yAxis: {
+      type: "value",
+      name: "kWh",
+    },
+    series: [
+      {
+        name: "鐢ㄧ數閲�",
+        type: chartType.value,
+        data: values,
+        smooth: true,
+        areaStyle: chartType.value === "line" ? { opacity: 0.15 } : undefined,
+        itemStyle: { color: "#409EFF" },
+        barMaxWidth: 40,
+      },
+    ],
+  });
+}
+
+function startAutoRefresh() {
+  stopAutoRefresh();
+  refreshTimer = setInterval(loadData, 60 * 1000);
+}
+
+function stopAutoRefresh() {
+  if (refreshTimer) {
+    clearInterval(refreshTimer);
+    refreshTimer = null;
+  }
+}
+
+function toggleAutoRefresh(val) {
+  if (val) {
+    startAutoRefresh();
+  } else {
+    stopAutoRefresh();
+  }
+}
+
+function handleResize() {
+  chartInstance?.resize();
+}
+
+onMounted(() => {
+  loadYesterday();
+  loadData();
+  startAutoRefresh();
+  window.addEventListener("resize", handleResize);
+});
+
+onBeforeUnmount(() => {
+  stopAutoRefresh();
+  window.removeEventListener("resize", handleResize);
+  chartInstance?.dispose();
+  chartInstance = null;
+});
+</script>
+
+<style scoped>
+.page-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: flex-start;
+  margin-bottom: 16px;
+}
+.page-header h2 {
+  margin: 0 0 4px;
+  font-size: 20px;
+}
+.subtitle {
+  margin: 0;
+  color: #909399;
+  font-size: 13px;
+}
+.header-actions {
+  display: flex;
+  align-items: center;
+  gap: 12px;
+}
+.update-time {
+  font-size: 13px;
+  color: #909399;
+}
+.yesterday-banner {
+  margin-bottom: 16px;
+}
+.yesterday-row {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+}
+.yesterday-title {
+  font-size: 14px;
+  color: #909399;
+  margin-bottom: 6px;
+}
+.yesterday-value {
+  font-size: 32px;
+  font-weight: 600;
+  color: #409eff;
+}
+.yesterday-value span {
+  font-size: 14px;
+  font-weight: 400;
+  color: #909399;
+}
+.yesterday-stats {
+  display: flex;
+  gap: 20px;
+  font-size: 13px;
+  color: #606266;
+}
+.monitor-cards {
+  margin-bottom: 16px;
+}
+.monitor-card {
+  text-align: center;
+  padding: 8px 0;
+}
+.monitor-title {
+  color: #909399;
+  font-size: 14px;
+  margin-bottom: 12px;
+}
+.monitor-value {
+  font-size: 32px;
+  font-weight: 600;
+  color: #303133;
+}
+.monitor-value .unit {
+  font-size: 14px;
+  font-weight: 400;
+  color: #909399;
+  margin-left: 4px;
+}
+.chart-card {
+  margin-bottom: 16px;
+}
+.card-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+}
+.chart-container {
+  width: 100%;
+  height: 400px;
+}
+</style>
diff --git a/src/views/energyManagement/energyStatistics/index.vue b/src/views/energyManagement/energyStatistics/index.vue
new file mode 100644
index 0000000..15889b5
--- /dev/null
+++ b/src/views/energyManagement/energyStatistics/index.vue
@@ -0,0 +1,426 @@
+<template>
+  <div class="app-container">
+    <!-- 鏄ㄦ棩鐢ㄧ數蹇 -->
+    <el-card class="yesterday-card" v-loading="yesterdayLoading">
+      <div class="yesterday-header">
+        <div>
+          <h3>鏄ㄦ棩鐢ㄧ數閲�</h3>
+          <p class="sub">{{ yesterdayLabel }}</p>
+        </div>
+        <el-button type="primary" link @click="viewYesterdayDetail">鏌ョ湅鏄ㄦ棩鏄庣粏</el-button>
+      </div>
+      <el-row :gutter="16">
+        <el-col :span="6">
+          <div class="metric-box highlight">
+            <div class="metric-label">鎬荤敤鐢甸噺</div>
+            <div class="metric-value">{{ yesterdaySummary.totalConsumption ?? 0 }} <span>kWh</span></div>
+          </div>
+        </el-col>
+        <el-col :span="6">
+          <div class="metric-box">
+            <div class="metric-label">骞冲潎鐢ㄧ數閲�</div>
+            <div class="metric-value">{{ yesterdaySummary.avgConsumption ?? 0 }} <span>kWh</span></div>
+          </div>
+        </el-col>
+        <el-col :span="6">
+          <div class="metric-box">
+            <div class="metric-label">鏈�澶х敤鐢甸噺</div>
+            <div class="metric-value">{{ yesterdaySummary.maxConsumption ?? 0 }} <span>kWh</span></div>
+          </div>
+        </el-col>
+        <el-col :span="6">
+          <div class="metric-box">
+            <div class="metric-label">鏈�灏忕敤鐢甸噺</div>
+            <div class="metric-value">{{ yesterdaySummary.minConsumption ?? 0 }} <span>kWh</span></div>
+          </div>
+        </el-col>
+      </el-row>
+    </el-card>
+
+    <el-card>
+      <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-radio-group v-model="queryForm.dimension" @change="handleDimensionChange">
+            <el-radio-button value="day">澶�</el-radio-button>
+            <el-radio-button value="month">鏈�</el-radio-button>
+            <el-radio-button value="quarter">瀛e害</el-radio-button>
+            <el-radio-button value="year">骞�</el-radio-button>
+          </el-radio-group>
+        </el-form-item>
+        <el-form-item label="鏃堕棿鑼冨洿">
+          <el-date-picker
+            v-if="queryForm.dimension === 'day'"
+            v-model="dayRange"
+            type="daterange"
+            range-separator="鑷�"
+            value-format="YYYY-MM-DD"
+          />
+          <el-date-picker
+            v-else-if="queryForm.dimension === 'month'"
+            v-model="monthRange"
+            type="monthrange"
+            range-separator="鑷�"
+            value-format="YYYY-MM"
+          />
+          <el-date-picker
+            v-else-if="queryForm.dimension === 'quarter'"
+            v-model="quarterRange"
+            type="daterange"
+            range-separator="鑷�"
+            value-format="YYYY-MM-DD"
+          />
+          <el-date-picker
+            v-else
+            v-model="yearRange"
+            type="yearrange"
+            range-separator="鑷�"
+            value-format="YYYY"
+          />
+        </el-form-item>
+        <el-form-item>
+          <el-button @click="setYesterday">鏄ㄦ棩</el-button>
+          <el-button @click="setLast7Days">杩�7澶�</el-button>
+          <el-button type="primary" :loading="loading" @click="handleQuery">鏌ヨ</el-button>
+          <el-button @click="handleExport">瀵煎嚭</el-button>
+        </el-form-item>
+      </el-form>
+
+      <el-row :gutter="16" class="summary-row">
+        <el-col :span="6">
+          <div class="summary-card total">
+            <div class="label">鎬荤敤鐢甸噺</div>
+            <div class="value">{{ summary.totalConsumption ?? 0 }} <span>kWh</span></div>
+          </div>
+        </el-col>
+        <el-col :span="6">
+          <div class="summary-card">
+            <div class="label">骞冲潎鐢ㄧ數閲�</div>
+            <div class="value">{{ summary.avgConsumption ?? 0 }} <span>kWh</span></div>
+          </div>
+        </el-col>
+        <el-col :span="6">
+          <div class="summary-card">
+            <div class="label">鏈�澶х敤鐢甸噺</div>
+            <div class="value">{{ summary.maxConsumption ?? 0 }} <span>kWh</span></div>
+          </div>
+        </el-col>
+        <el-col :span="6">
+          <div class="summary-card">
+            <div class="label">鏈�灏忕敤鐢甸噺</div>
+            <div class="value">{{ summary.minConsumption ?? 0 }} <span>kWh</span></div>
+          </div>
+        </el-col>
+      </el-row>
+
+      <div class="chart-toolbar">
+        <span>瓒嬪娍鍥�</span>
+        <el-radio-group v-model="chartType" size="small" @change="renderChart">
+          <el-radio-button value="line">鎶樼嚎鍥�</el-radio-button>
+          <el-radio-button value="bar">鏌辩姸鍥�</el-radio-button>
+        </el-radio-group>
+      </div>
+      <div ref="chartRef" class="chart-container"></div>
+
+      <div class="detail-title">鐢ㄧ數鏄庣粏</div>
+      <el-table v-loading="loading" :data="detailRecords" border stripe max-height="360">
+        <el-table-column label="鏃堕棿" min-width="150">
+          <template #default="{ row }">
+            {{ parseTimeKey(row.timeKey, queryForm.dimension) }}
+          </template>
+        </el-table-column>
+        <el-table-column prop="meterId" label="鐢佃〃ID" width="100" />
+        <el-table-column prop="address" label="琛ㄥ湴鍧�" min-width="120" show-overflow-tooltip />
+        <el-table-column prop="collectorNo" label="閲囬泦鍣ㄥ彿" min-width="120" show-overflow-tooltip />
+        <el-table-column prop="totalConsumption" label="鎬荤數閲�(kWh)" width="120" />
+        <el-table-column prop="peakConsumption" label="宄�(kWh)" width="100" />
+        <el-table-column prop="flatConsumption" label="骞�(kWh)" width="100" />
+        <el-table-column prop="valleyConsumption" label="璋�(kWh)" width="100" />
+        <el-table-column prop="startTime" label="寮�濮嬫椂闂�" min-width="150" />
+        <el-table-column prop="endTime" label="缁撴潫鏃堕棿" min-width="150" />
+      </el-table>
+    </el-card>
+  </div>
+</template>
+
+<script setup>
+import { computed, getCurrentInstance, onBeforeUnmount, onMounted, reactive, ref } from "vue";
+import { ElMessageBox } from "element-plus";
+import * as echarts from "echarts";
+import {
+  summaryStatisticEle,
+  getYesterdaySummary,
+  formatDayPicker,
+  formatDayTime,
+  formatMonthTime,
+  getYesterdayDayPicker,
+  parseTimeKey,
+} from "@/api/energyManagement/statisticEle.js";
+
+const { proxy } = getCurrentInstance();
+
+const loading = ref(false);
+const yesterdayLoading = ref(false);
+const chartRef = ref(null);
+let chartInstance = null;
+
+const queryForm = reactive({ dimension: "day" });
+const chartType = ref("bar");
+const summary = ref({});
+const chartRecords = ref([]);
+const detailRecords = ref([]);
+const yesterdaySummary = ref({});
+
+const dayRange = ref([]);
+const monthRange = ref([]);
+const quarterRange = ref([]);
+const yearRange = ref([]);
+
+const yesterdayLabel = computed(() => getYesterdayDayPicker());
+
+function initDefaultRange() {
+  const yesterday = getYesterdayDayPicker();
+  dayRange.value = [yesterday, yesterday];
+  const now = new Date();
+  const weekAgo = new Date(now.getTime() - 7 * 86400000);
+  quarterRange.value = [formatDayPicker(weekAgo), formatDayPicker(now)];
+  monthRange.value = [
+    formatMonthTime(now).replace(/(\d{4})(\d{2})/, "$1-$2"),
+    formatMonthTime(now).replace(/(\d{4})(\d{2})/, "$1-$2"),
+  ];
+  yearRange.value = [String(now.getFullYear()), String(now.getFullYear())];
+}
+
+function buildTimeParams() {
+  const dim = queryForm.dimension;
+  if (dim === "day") {
+    return {
+      startTime: dayRange.value[0].replace(/-/g, ""),
+      endTime: dayRange.value[1].replace(/-/g, ""),
+    };
+  }
+  if (dim === "month") {
+    return {
+      startTime: monthRange.value[0].replace(/-/g, ""),
+      endTime: monthRange.value[1].replace(/-/g, ""),
+    };
+  }
+  if (dim === "quarter") {
+    return {
+      startTime: quarterRange.value[0].replace(/-/g, ""),
+      endTime: quarterRange.value[1].replace(/-/g, ""),
+    };
+  }
+  return {
+    startTime: yearRange.value[0],
+    endTime: yearRange.value[1],
+  };
+}
+
+async function loadYesterday() {
+  yesterdayLoading.value = true;
+  try {
+    const res = await getYesterdaySummary();
+    yesterdaySummary.value = res.data || {};
+  } finally {
+    yesterdayLoading.value = false;
+  }
+}
+
+async function handleQuery() {
+  loading.value = true;
+  try {
+    const params = { dimension: queryForm.dimension, ...buildTimeParams() };
+    const res = await summaryStatisticEle(params);
+    summary.value = res.data || {};
+    chartRecords.value = res.data?.chartRecords || [];
+    detailRecords.value = res.data?.records || [];
+    renderChart();
+  } finally {
+    loading.value = false;
+  }
+}
+
+function renderChart() {
+  if (!chartRef.value) return;
+  if (!chartInstance) {
+    chartInstance = echarts.init(chartRef.value);
+  }
+  const labels = chartRecords.value.map((item) =>
+    parseTimeKey(item.timeKey, queryForm.dimension)
+  );
+  const values = chartRecords.value.map((item) => item.totalConsumption || 0);
+  chartInstance.setOption({
+    tooltip: { trigger: "axis" },
+    grid: { left: 50, right: 20, top: 30, bottom: 50 },
+    xAxis: { type: "category", data: labels, axisLabel: { rotate: 30, fontSize: 11 } },
+    yAxis: { type: "value", name: "kWh" },
+    series: [
+      {
+        name: "鎬荤敤鐢甸噺",
+        type: chartType.value,
+        data: values,
+        smooth: chartType.value === "line",
+        areaStyle: chartType.value === "line" ? { opacity: 0.12 } : undefined,
+        itemStyle: { color: "#409EFF" },
+        barMaxWidth: 40,
+      },
+    ],
+  });
+}
+
+function setYesterday() {
+  queryForm.dimension = "day";
+  const yesterday = getYesterdayDayPicker();
+  dayRange.value = [yesterday, yesterday];
+  handleQuery();
+}
+
+function setLast7Days() {
+  queryForm.dimension = "day";
+  const now = new Date();
+  const weekAgo = new Date(now.getTime() - 6 * 86400000);
+  dayRange.value = [formatDayPicker(weekAgo), formatDayPicker(now)];
+  handleQuery();
+}
+
+function viewYesterdayDetail() {
+  setYesterday();
+}
+
+function handleDimensionChange() {
+  handleQuery();
+}
+
+function handleExport() {
+  ElMessageBox.confirm("纭瀵煎嚭褰撳墠缁熻鎶ヨ〃锛�", "瀵煎嚭", { type: "warning" })
+    .then(() => {
+      proxy.download("/statisticEle/export", {
+        dimension: queryForm.dimension,
+        ...buildTimeParams(),
+      }, `鑳借�楃粺璁${queryForm.dimension}.xlsx`);
+    })
+    .catch(() => {});
+}
+
+function handleResize() {
+  chartInstance?.resize();
+}
+
+onMounted(() => {
+  initDefaultRange();
+  loadYesterday();
+  handleQuery();
+  window.addEventListener("resize", handleResize);
+});
+
+onBeforeUnmount(() => {
+  window.removeEventListener("resize", handleResize);
+  chartInstance?.dispose();
+  chartInstance = null;
+});
+</script>
+
+<style scoped>
+.yesterday-card {
+  margin-bottom: 16px;
+}
+.yesterday-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: flex-start;
+  margin-bottom: 16px;
+}
+.yesterday-header h3 {
+  margin: 0 0 4px;
+  font-size: 18px;
+}
+.yesterday-header .sub {
+  margin: 0;
+  color: #909399;
+  font-size: 13px;
+}
+.metric-box {
+  background: #f5f7fa;
+  border-radius: 8px;
+  padding: 16px;
+  text-align: center;
+}
+.metric-box.highlight {
+  background: linear-gradient(135deg, #409eff22, #409eff11);
+}
+.metric-label {
+  font-size: 13px;
+  color: #909399;
+  margin-bottom: 8px;
+}
+.metric-value {
+  font-size: 24px;
+  font-weight: 600;
+}
+.metric-value span {
+  font-size: 13px;
+  font-weight: 400;
+  color: #909399;
+}
+.card-header {
+  display: flex;
+  align-items: center;
+  gap: 12px;
+}
+.card-header .desc {
+  font-size: 13px;
+  color: #909399;
+}
+.search-form {
+  margin-bottom: 16px;
+}
+.summary-row {
+  margin-bottom: 16px;
+}
+.summary-card {
+  background: #f5f7fa;
+  border-radius: 8px;
+  padding: 20px;
+  text-align: center;
+}
+.summary-card.total {
+  background: linear-gradient(135deg, #409eff22, #409eff11);
+}
+.summary-card .label {
+  font-size: 13px;
+  color: #909399;
+  margin-bottom: 8px;
+}
+.summary-card .value {
+  font-size: 26px;
+  font-weight: 600;
+}
+.summary-card .value span {
+  font-size: 13px;
+  font-weight: 400;
+  color: #909399;
+}
+.chart-toolbar {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 8px;
+  font-weight: 500;
+}
+.chart-container {
+  width: 100%;
+  height: 380px;
+  margin-bottom: 20px;
+}
+.detail-title {
+  font-weight: 500;
+  margin-bottom: 10px;
+}
+</style>
diff --git a/src/views/energyManagement/meterArchive/components/formDia.vue b/src/views/energyManagement/meterArchive/components/formDia.vue
new file mode 100644
index 0000000..c2f77ec
--- /dev/null
+++ b/src/views/energyManagement/meterArchive/components/formDia.vue
@@ -0,0 +1,114 @@
+<template>
+  <el-dialog v-model="visible" :title="title" width="520px" @close="closeDia">
+    <el-form ref="formRef" :model="form" :rules="rules" label-width="100px">
+      <el-form-item label="鐢佃〃鍚嶇О" prop="meterName">
+        <el-input v-model="form.meterName" placeholder="璇疯緭鍏ョ數琛ㄥ悕绉�" />
+      </el-form-item>
+      <el-form-item label="琛ㄥ湴鍧�" prop="address">
+        <el-input v-model="form.address" placeholder="璇疯緭鍏ヨ〃鍦板潃" />
+      </el-form-item>
+      <el-form-item label="澶囨敞">
+        <el-input v-model="form.description" type="textarea" :rows="2" placeholder="澶囨敞" />
+      </el-form-item>
+      <el-form-item label="缁х數鍣ㄧ姸鎬�" prop="relayState">
+        <el-select v-model="form.relayState" style="width: 100%">
+          <el-option label="鍚堥椄" value="1" />
+          <el-option label="鎷夐椄" value="0" />
+        </el-select>
+      </el-form-item>
+      <el-form-item v-if="showRate" label="鍊嶇巼">
+        <el-input-number v-model="form.rate" :min="1" style="width: 100%" />
+      </el-form-item>
+      <el-form-item v-if="operationType === 'edit' && form.source === 'sync'" label="妗fID">
+        <el-input :model-value="form.meterId" disabled />
+      </el-form-item>
+      <el-form-item v-if="operationType === 'edit' && form.source === 'sync'">
+        <el-text type="info" size="small">鍚屾鐢佃〃浠呭彲淇敼鍚嶇О銆佽〃鍦板潃銆佸娉ㄣ�佺户鐢靛櫒鐘舵�侊紙涓嶅悓姝ュ埌鑳芥簮骞冲彴锛�</el-text>
+      </el-form-item>
+    </el-form>
+    <template #footer>
+      <el-button @click="visible = false">鍙栨秷</el-button>
+      <el-button type="primary" :loading="submitting" @click="submit">纭畾</el-button>
+    </template>
+  </el-dialog>
+</template>
+
+<script setup>
+import { computed, reactive, ref } from "vue";
+import { ElMessage } from "element-plus";
+import { meterAdd, meterUpdate } from "@/api/energyManagement/tqdianbiao.js";
+
+const emit = defineEmits(["close"]);
+const visible = ref(false);
+const submitting = ref(false);
+const operationType = ref("add");
+const title = ref("");
+const formRef = ref(null);
+
+const defaultForm = () => ({
+  id: null,
+  meterId: null,
+  meterName: "",
+  address: "",
+  description: "",
+  relayState: "1",
+  rate: 1,
+  source: "manual",
+});
+
+const form = reactive(defaultForm());
+
+const showRate = computed(() => operationType.value === "edit" && form.source === "manual");
+
+const rules = {
+  address: [{ required: true, message: "璇疯緭鍏ヨ〃鍦板潃", trigger: "blur" }],
+  relayState: [{ required: true, message: "璇烽�夋嫨缁х數鍣ㄧ姸鎬�", trigger: "change" }],
+};
+
+function open(type, row) {
+  operationType.value = type;
+  title.value = type === "add" ? "鏂板鐢佃〃" : "缂栬緫鐢佃〃";
+  Object.assign(form, defaultForm());
+  if (type === "edit" && row) {
+    Object.assign(form, {
+      id: row.id,
+      meterId: row.meterId,
+      meterName: row.meterName || row.address || "",
+      address: row.address || "",
+      description: row.description || "",
+      relayState: row.relayState || "1",
+      rate: row.rate || 1,
+      source: row.source || "sync",
+    });
+  }
+  visible.value = true;
+}
+
+function closeDia() {
+  emit("close");
+}
+
+async function submit() {
+  await formRef.value.validate();
+  submitting.value = true;
+  try {
+    const payload = { ...form };
+    if (!payload.meterName) {
+      payload.meterName = payload.address;
+    }
+    if (operationType.value === "add") {
+      await meterAdd(payload);
+      ElMessage.success("鏂板鎴愬姛");
+    } else {
+      await meterUpdate(payload);
+      ElMessage.success("淇敼鎴愬姛");
+    }
+    visible.value = false;
+    emit("close");
+  } finally {
+    submitting.value = false;
+  }
+}
+
+defineExpose({ open });
+</script>
diff --git a/src/views/energyManagement/meterArchive/index.vue b/src/views/energyManagement/meterArchive/index.vue
new file mode 100644
index 0000000..d22b847
--- /dev/null
+++ b/src/views/energyManagement/meterArchive/index.vue
@@ -0,0 +1,124 @@
+<template>
+  <div class="app-container">
+    <div class="search_form">
+      <div>
+        <span class="search_title">鍏抽敭璇嶏細</span>
+        <el-input
+          v-model="searchForm.keyword"
+          style="width: 240px"
+          placeholder="鐢佃〃鍚嶇О/琛ㄥ湴鍧�/澶囨敞"
+          clearable
+          @keyup.enter="handleQuery"
+        />
+        <el-button type="primary" @click="handleQuery" style="margin-left: 10px">鎼滅储</el-button>
+      </div>
+      <div>
+        <el-button type="primary" @click="openForm('add')">鏂板鐢佃〃</el-button>
+        <el-button type="success" :loading="syncing" @click="handleSync">鍚屾鐢佃〃</el-button>
+      </div>
+    </div>
+    <div class="table_list">
+      <PIMTable
+        rowKey="id"
+        :column="tableColumn"
+        :tableData="tableData"
+        :page="page"
+        :tableLoading="tableLoading"
+        @pagination="pagination"
+      >
+        <template #source="{ row }">
+          <el-tag :type="row.source === 'manual' ? 'warning' : 'info'" size="small">
+            {{ row.source === "manual" ? "鎵嬪姩" : "鍚屾" }}
+          </el-tag>
+        </template>
+        <template #relayState="{ row }">
+          <el-tag :type="row.relayState === '1' ? 'success' : 'danger'" size="small">
+            {{ row.relayState === "1" ? "鍚堥椄" : row.relayState === "0" ? "鎷夐椄" : "鏈煡" }}
+          </el-tag>
+        </template>
+        <template #operate="{ row }">
+          <el-button link type="primary" @click="openForm('edit', row)">缂栬緫</el-button>
+          <el-button v-if="row.source === 'manual'" link type="danger" @click="handleDelete(row)">鍒犻櫎</el-button>
+        </template>
+      </PIMTable>
+    </div>
+    <form-dia ref="formDiaRef" @close="getList" />
+  </div>
+</template>
+
+<script setup>
+import { onMounted, reactive, ref, toRefs } from "vue";
+import { ElMessage, ElMessageBox } from "element-plus";
+import FormDia from "./components/formDia.vue";
+import { meterListPage, meterSync, meterDelete } from "@/api/energyManagement/tqdianbiao.js";
+
+const tableLoading = ref(false);
+const syncing = ref(false);
+const tableData = ref([]);
+const formDiaRef = ref(null);
+
+const data = reactive({ searchForm: { keyword: "" } });
+const { searchForm } = toRefs(data);
+const page = reactive({ current: 1, size: 10, total: 0 });
+
+const tableColumn = ref([
+  { label: "鐢佃〃鍚嶇О", prop: "meterName", minWidth: 120 },
+  { label: "鐢佃〃妗fID", prop: "meterId", width: 120 },
+  { label: "琛ㄥ湴鍧�", prop: "address", minWidth: 120 },
+  { label: "鍊嶇巼", prop: "rate", width: 70 },
+  { label: "鏉ユ簮", prop: "source", dataType: "slot", slot: "source", width: 80 },
+  { label: "缁х數鍣�", prop: "relayState", dataType: "slot", slot: "relayState", width: 90 },
+  { label: "澶囨敞", prop: "description", minWidth: 100 },
+  { label: "鍚屾鏃堕棿", prop: "syncTime", minWidth: 160 },
+  { label: "鎿嶄綔", prop: "operate", dataType: "slot", slot: "operate", width: 120, fixed: "right" },
+]);
+
+function openForm(type, row) {
+  formDiaRef.value.open(type, row);
+}
+
+function handleQuery() {
+  page.current = 1;
+  getList();
+}
+
+function pagination(obj) {
+  page.current = obj.page;
+  page.size = obj.limit;
+  getList();
+}
+
+async function getList() {
+  tableLoading.value = true;
+  try {
+    const res = await meterListPage({ ...searchForm.value, current: page.current, size: page.size });
+    tableData.value = res.data.records;
+    page.total = res.data.total;
+  } finally {
+    tableLoading.value = false;
+  }
+}
+
+async function handleSync() {
+  syncing.value = true;
+  try {
+    const res = await meterSync();
+    ElMessage.success(res.msg || "鍚屾鎴愬姛");
+    getList();
+  } finally {
+    syncing.value = false;
+  }
+}
+
+function handleDelete(row) {
+  ElMessageBox.confirm(`纭鍒犻櫎鐢佃〃銆�${row.meterName || row.address}銆嶏紵`, "鎻愮ず", { type: "warning" })
+    .then(async () => {
+      await meterDelete([row.id]);
+      ElMessage.success("鍒犻櫎鎴愬姛");
+      getList();
+    })
+    .catch(() => {});
+}
+
+onMounted(() => getList());
+</script>

--
Gitblit v1.9.3