| ¶Ô±ÈÐÂÎļþ |
| | |
| | | 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()); |
| | | } |
| | | |
| | | /** è§£ææ¶é´æ è¯ä¸ºå¯è¯»æ ¼å¼ */ |
| | | 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}å£åº¦`; |
| | | } |
| | | 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), |
| | | }; |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | 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" }); |
| | | } |
| | | |
| | | // ========== çµè¡¨æ¡£æ¡ ========== |
| | | 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 }); |
| | | } |
| | |
| | | <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> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <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="éé卿¡£æ¡ID" 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: "请è¾å
¥éé卿¡£æ¡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> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <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="ééå¨å·/æ¡£æ¡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: "éé卿¡£æ¡ID", 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> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <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> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <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="æ¡£æ¡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> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <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">忥尿¶çµé + æå¨æè¡¨ï¼ignore_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="æ¡£æ¡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> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <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> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <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">å£åº¦</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> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <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="æ¡£æ¡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> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <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: "çµè¡¨æ¡£æ¡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> |