| ¶Ô±ÈÐÂÎļþ |
| | |
| | | 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 analyticsStatisticEle(query) { |
| | | return request({ |
| | | url: "/statisticEle/analytics", |
| | | 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 === "week" && timeKey.includes("W")) { |
| | | const [y, w] = timeKey.split("W"); |
| | | return `${y}å¹´ 第${Number(w)}å¨`; |
| | | } |
| | | 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" shadow="never"> |
| | | <el-row :gutter="16"> |
| | | <el-col :span="8"> |
| | | <div class="sync-item"> |
| | | <div class="sync-label">çµè¡¨æ°é</div> |
| | | <div class="sync-value">{{ syncStatus.meterCount ?? 0 }}</div> |
| | | </div> |
| | | </el-col> |
| | | <el-col :span="8"> |
| | | <div class="sync-item"> |
| | | <div class="sync-label">æè¿åæ¥æ¶é´</div> |
| | | <div class="sync-value sync-time">{{ lastHourSync }}</div> |
| | | </div> |
| | | </el-col> |
| | | <el-col :span="8"> |
| | | <div class="sync-item"> |
| | | <div class="sync-label">çµéè®°å½æ°</div> |
| | | <div class="sync-value">{{ syncStatus.recordCountByDimension?.hour ?? 0 }}</div> |
| | | </div> |
| | | </el-col> |
| | | </el-row> |
| | | </el-card> |
| | | |
| | | <el-tabs v-model="activeTab"> |
| | | <el-tab-pane label="çµéæ°æ®" name="record"> |
| | | <el-card shadow="never"> |
| | | <template #header> |
| | | <div class="card-header"> |
| | | <span>è½èæ°æ®éé</span> |
| | | <span class="desc">å±ç¤ºåæ¥å°æ¶çµéï¼æ¯ææå¨æè¡¨è¡¥å½</span> |
| | | </div> |
| | | </template> |
| | | |
| | | <el-form :inline="true" class="search-form"> |
| | | <el-form-item label="æ¶é´èå´"> |
| | | <el-date-picker |
| | | v-model="hourRange" |
| | | type="datetimerange" |
| | | range-separator="è³" |
| | | start-placeholder="å¼å§æ¶é´" |
| | | end-placeholder="ç»ææ¶é´" |
| | | value-format="YYYY-MM-DD HH:mm:ss" |
| | | :default-time="defaultTime" |
| | | /> |
| | | </el-form-item> |
| | | <el-form-item> |
| | | <el-button type="primary" :loading="loading" @click="handleRefresh"> |
| | | <el-icon><Refresh /></el-icon> |
| | | å·æ° |
| | | </el-button> |
| | | <el-button type="primary" @click="openRecordForm('add')">æå¨æè¡¨</el-button> |
| | | <el-button type="danger" plain :disabled="!selectedRows.length" @click="handleDelete">å é¤</el-button> |
| | | </el-form-item> |
| | | </el-form> |
| | | |
| | | <el-row :gutter="16" class="stat-row"> |
| | | <el-col :span="8"> |
| | | <div class="stat-item"> |
| | | <div class="stat-label">è®°å½æ¡æ°</div> |
| | | <div class="stat-value">{{ tableData.length }}</div> |
| | | </div> |
| | | </el-col> |
| | | <el-col :span="8"> |
| | | <div class="stat-item"> |
| | | <div class="stat-label">æ»ç¨çµé(kWh)</div> |
| | | <div class="stat-value">{{ totalConsumption }}</div> |
| | | </div> |
| | | </el-col> |
| | | <el-col :span="8"> |
| | | <div class="stat-item"> |
| | | <div class="stat-label">æ¶åçµè¡¨</div> |
| | | <div class="stat-value">{{ meterCount }}</div> |
| | | </div> |
| | | </el-col> |
| | | </el-row> |
| | | |
| | | <el-table |
| | | v-loading="loading" |
| | | :data="pagedData" |
| | | border |
| | | stripe |
| | | height="calc(100vh - 480px)" |
| | | @selection-change="handleSelectionChange" |
| | | > |
| | | <el-table-column type="selection" width="50" /> |
| | | <el-table-column label="æ¶é´" min-width="150"> |
| | | <template #default="{ row }"> |
| | | {{ formatRecordTime(row) }} |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column prop="meterName" label="çµè¡¨åç§°" min-width="120" show-overflow-tooltip /> |
| | | <el-table-column prop="meterId" label="çµè¡¨ID" width="100" /> |
| | | <el-table-column prop="address" label="表å°å" min-width="110" show-overflow-tooltip /> |
| | | <el-table-column prop="prevReading" label="䏿¬¡çµé" width="100" /> |
| | | <el-table-column prop="currReading" label="æ¬æ¬¡çµé" width="100" /> |
| | | <el-table-column prop="ratio" label="åç" width="70" /> |
| | | <el-table-column prop="totalConsumption" label="æ¬æ¬¡ç¨çµé(kWh)" width="130" /> |
| | | <el-table-column label="æè¡¨æ¹å¼" width="90"> |
| | | <template #default="{ row }"> |
| | | <el-tag :type="row.readingMethod === 'manual' ? 'warning' : 'success'" size="small"> |
| | | {{ row.readingMethod === "manual" ? "æå¨" : "忥" }} |
| | | </el-tag> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="æä½" width="80" fixed="right"> |
| | | <template #default="{ row }"> |
| | | <el-button |
| | | v-if="row.readingMethod === 'manual'" |
| | | link |
| | | type="primary" |
| | | @click="openRecordForm('edit', row)" |
| | | >ç¼è¾</el-button> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | |
| | | <pagination |
| | | v-show="tableData.length > 0" |
| | | :total="tableData.length" |
| | | :page="page.current" |
| | | :limit="page.size" |
| | | :page-sizes="[50, 100, 200, 500]" |
| | | @pagination="handlePagination" |
| | | /> |
| | | </el-card> |
| | | </el-tab-pane> |
| | | |
| | | <el-tab-pane label="çµè¡¨ç®¡ç" name="meter"> |
| | | <el-card shadow="never"> |
| | | <div class="meter-toolbar"> |
| | | <el-input |
| | | v-model="meterKeyword" |
| | | placeholder="æç´¢çµè¡¨åç§°/å°å/夿³¨" |
| | | clearable |
| | | style="width: 240px" |
| | | @keyup.enter="loadMeters" |
| | | /> |
| | | <el-button type="primary" @click="loadMeters">æç´¢</el-button> |
| | | <el-button type="success" :loading="meterSyncing" @click="handleMeterSync">忥çµè¡¨</el-button> |
| | | <el-button type="primary" @click="openMeterForm('add')">æ°å¢çµè¡¨</el-button> |
| | | </div> |
| | | <el-table v-loading="meterLoading" :data="meterTableData" border stripe height="calc(100vh - 420px)"> |
| | | <el-table-column label="çµè¡¨åç§°" min-width="120" show-overflow-tooltip> |
| | | <template #default="{ row }">{{ row.meterName || row.address || "-" }}</template> |
| | | </el-table-column> |
| | | <el-table-column prop="meterId" label="æ¡£æ¡ID" width="110" /> |
| | | <el-table-column prop="address" label="表å°å" min-width="120" /> |
| | | <el-table-column prop="rate" label="åç" width="70" /> |
| | | <el-table-column label="æ¥æº" width="80"> |
| | | <template #default="{ row }"> |
| | | <el-tag :type="row.source === 'manual' ? 'warning' : 'info'" size="small"> |
| | | {{ row.source === "manual" ? "æå¨" : "忥" }} |
| | | </el-tag> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="ç»§çµå¨" width="80"> |
| | | <template #default="{ row }"> |
| | | <el-tag :type="row.relayState === '1' ? 'success' : 'danger'" size="small"> |
| | | {{ row.relayState === "1" ? "åé¸" : "æé¸" }} |
| | | </el-tag> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column prop="description" label="夿³¨" min-width="100" show-overflow-tooltip /> |
| | | <el-table-column label="æä½" width="140" fixed="right"> |
| | | <template #default="{ row }"> |
| | | <el-button link type="primary" @click="openMeterForm('edit', row)">ç¼è¾</el-button> |
| | | <el-button v-if="row.source === 'manual'" link type="danger" @click="handleMeterDelete(row)">å é¤</el-button> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | <pagination |
| | | v-show="meterPage.total > 0" |
| | | :total="meterPage.total" |
| | | :page="meterPage.current" |
| | | :limit="meterPage.size" |
| | | :page-sizes="[50, 100, 200, 500]" |
| | | @pagination="handleMeterPagination" |
| | | /> |
| | | </el-card> |
| | | </el-tab-pane> |
| | | </el-tabs> |
| | | |
| | | <form-dia ref="formDiaRef" @close="handleRefresh" /> |
| | | <meter-form-dia ref="meterFormDiaRef" @close="loadMeters" /> |
| | | </div> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { computed, onMounted, reactive, ref } from "vue"; |
| | | import { Refresh } from "@element-plus/icons-vue"; |
| | | import { ElMessage, ElMessageBox } from "element-plus"; |
| | | import FormDia from "./components/formDia.vue"; |
| | | import MeterFormDia from "./components/meterFormDia.vue"; |
| | | import { eleRecordDelete, meterListPage, meterSync, meterDelete } from "@/api/energyManagement/tqdianbiao.js"; |
| | | import { |
| | | listStatisticEle, |
| | | getSyncStatus, |
| | | formatMinuteTime, |
| | | parseTimeKey, |
| | | getRecentHourRange, |
| | | } from "@/api/energyManagement/statisticEle.js"; |
| | | |
| | | const activeTab = ref("record"); |
| | | const loading = ref(false); |
| | | const tableData = ref([]); |
| | | const syncStatus = ref({}); |
| | | const selectedRows = ref([]); |
| | | const formDiaRef = ref(null); |
| | | const meterFormDiaRef = ref(null); |
| | | |
| | | const page = reactive({ current: 1, size: 500 }); |
| | | const defaultTime = [ |
| | | new Date(2000, 0, 1, 0, 0, 0), |
| | | new Date(2000, 0, 1, 23, 59, 59), |
| | | ]; |
| | | const hourRange = ref([]); |
| | | |
| | | const meterLoading = ref(false); |
| | | const meterSyncing = ref(false); |
| | | const meterKeyword = ref(""); |
| | | const meterTableData = ref([]); |
| | | const meterPage = reactive({ current: 1, size: 500, total: 0 }); |
| | | |
| | | const lastHourSync = computed(() => syncStatus.value.lastSyncTimeByType?.hour || "-"); |
| | | |
| | | const totalConsumption = computed(() => { |
| | | return tableData.value.reduce((sum, item) => sum + (item.totalConsumption || 0), 0).toFixed(2); |
| | | }); |
| | | |
| | | const meterCount = computed(() => new Set(tableData.value.map((item) => item.meterId)).size); |
| | | |
| | | const pagedData = computed(() => { |
| | | const start = (page.current - 1) * page.size; |
| | | return tableData.value.slice(start, start + page.size); |
| | | }); |
| | | |
| | | function formatRecordTime(row) { |
| | | const dim = row.readingMethod === "manual" ? "manual" : "hour"; |
| | | if (row.timeKey?.length === 12) return parseTimeKey(row.timeKey, "manual"); |
| | | return parseTimeKey(row.timeKey, dim); |
| | | } |
| | | |
| | | function initDefaultRange() { |
| | | const now = new Date(); |
| | | const start = new Date(now.getTime() - 7 * 86400000); |
| | | hourRange.value = [ |
| | | `${start.getFullYear()}-${String(start.getMonth() + 1).padStart(2, "0")}-${String(start.getDate()).padStart(2, "0")} 00:00:00`, |
| | | `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}-${String(now.getDate()).padStart(2, "0")} 23:59:59`, |
| | | ]; |
| | | } |
| | | |
| | | function buildTimeParams() { |
| | | if (!hourRange.value || hourRange.value.length !== 2) { |
| | | return { ...getRecentHourRange(24 * 7), ignoreRadio: 1 }; |
| | | } |
| | | return { |
| | | startTime: formatMinuteTime(new Date(hourRange.value[0])), |
| | | endTime: formatMinuteTime(new Date(hourRange.value[1])), |
| | | ignoreRadio: 1, |
| | | }; |
| | | } |
| | | |
| | | async function loadSyncStatus() { |
| | | const res = await getSyncStatus(); |
| | | syncStatus.value = res.data || {}; |
| | | } |
| | | |
| | | async function fetchData() { |
| | | loading.value = true; |
| | | try { |
| | | const params = { dimension: "hour", ...buildTimeParams() }; |
| | | const res = await listStatisticEle(params); |
| | | tableData.value = res.data || []; |
| | | page.current = 1; |
| | | } finally { |
| | | loading.value = false; |
| | | } |
| | | } |
| | | |
| | | async function loadMeters() { |
| | | meterLoading.value = true; |
| | | try { |
| | | const res = await meterListPage({ |
| | | keyword: meterKeyword.value, |
| | | current: meterPage.current, |
| | | size: meterPage.size, |
| | | }); |
| | | meterTableData.value = res.data.records || []; |
| | | meterPage.total = res.data.total || 0; |
| | | } finally { |
| | | meterLoading.value = false; |
| | | } |
| | | } |
| | | |
| | | function handleSelectionChange(rows) { |
| | | selectedRows.value = rows; |
| | | } |
| | | |
| | | function openRecordForm(type, row) { |
| | | formDiaRef.value.open(type, row); |
| | | } |
| | | |
| | | function openMeterForm(type, row) { |
| | | meterFormDiaRef.value.open(type, row); |
| | | } |
| | | |
| | | function handleDelete() { |
| | | ElMessageBox.confirm("确认å é¤éä¸ççµéè®°å½ï¼", "æç¤º", { type: "warning" }) |
| | | .then(async () => { |
| | | await eleRecordDelete(selectedRows.value.map((r) => r.id)); |
| | | ElMessage.success("å 餿å"); |
| | | handleRefresh(); |
| | | }) |
| | | .catch(() => {}); |
| | | } |
| | | |
| | | function handleMeterDelete(row) { |
| | | ElMessageBox.confirm(`确认å é¤çµè¡¨ã${row.meterName}ãï¼`, "æç¤º", { type: "warning" }) |
| | | .then(async () => { |
| | | await meterDelete([row.id]); |
| | | ElMessage.success("å 餿å"); |
| | | loadMeters(); |
| | | }) |
| | | .catch(() => {}); |
| | | } |
| | | |
| | | async function handleMeterSync() { |
| | | meterSyncing.value = true; |
| | | try { |
| | | const res = await meterSync(); |
| | | ElMessage.success(res.msg || "忥æå"); |
| | | loadMeters(); |
| | | loadSyncStatus(); |
| | | } finally { |
| | | meterSyncing.value = false; |
| | | } |
| | | } |
| | | |
| | | function handleRefresh() { |
| | | loadSyncStatus(); |
| | | fetchData(); |
| | | } |
| | | |
| | | function handlePagination(obj) { |
| | | page.current = obj.page; |
| | | page.size = obj.limit; |
| | | } |
| | | |
| | | function handleMeterPagination(obj) { |
| | | meterPage.current = obj.page; |
| | | meterPage.size = obj.limit; |
| | | loadMeters(); |
| | | } |
| | | |
| | | onMounted(() => { |
| | | initDefaultRange(); |
| | | handleRefresh(); |
| | | loadMeters(); |
| | | }); |
| | | </script> |
| | | |
| | | <style scoped> |
| | | .sync-card { margin-bottom: 16px; } |
| | | .sync-item { |
| | | background: #f5f7fa; |
| | | border-radius: 8px; |
| | | padding: 16px; |
| | | text-align: center; |
| | | height: 100%; |
| | | } |
| | | .sync-label { font-size: 13px; color: #909399; margin-bottom: 8px; } |
| | | .sync-value { font-size: 24px; font-weight: 600; color: #303133; } |
| | | .sync-value.sync-time { font-size: 14px; font-weight: 500; color: #606266; line-height: 1.4; word-break: break-all; } |
| | | .card-header { display: flex; align-items: center; gap: 12px; } |
| | | .card-header .desc { font-size: 13px; color: #909399; } |
| | | .search-form { margin-bottom: 16px; } |
| | | .stat-row { margin-bottom: 16px; } |
| | | .stat-item { background: #f5f7fa; border-radius: 8px; padding: 16px; text-align: center; } |
| | | .stat-label { font-size: 13px; color: #909399; margin-bottom: 8px; } |
| | | .stat-value { font-size: 24px; font-weight: 600; } |
| | | .meter-toolbar { display: flex; gap: 10px; margin-bottom: 16px; align-items: center; } |
| | | </style> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <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> |
| | | <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="week">å¨</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="æ¶é´èå´" class="time-range-item"> |
| | | <el-date-picker |
| | | v-if="queryForm.dimension === 'day' || queryForm.dimension === 'week'" |
| | | v-model="dayRange" |
| | | type="daterange" |
| | | range-separator="è³" |
| | | value-format="YYYY-MM-DD" |
| | | :shortcuts="dayShortcuts" |
| | | /> |
| | | <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 label="è¶å¿ç²åº¦"> |
| | | <el-radio-group v-model="queryForm.trendGranularity" size="small" @change="handleQuery"> |
| | | <el-radio-button value="hour">å°æ¶</el-radio-button> |
| | | <el-radio-button value="day">æ¥</el-radio-button> |
| | | <el-radio-button value="week">å¨</el-radio-button> |
| | | <el-radio-button value="month">æ</el-radio-button> |
| | | </el-radio-group> |
| | | </el-form-item> |
| | | <el-form-item> |
| | | <el-button type="primary" :loading="loading" @click="handleQuery">æ¥è¯¢</el-button> |
| | | <el-button @click="handleExport">导åº</el-button> |
| | | </el-form-item> |
| | | </el-form> |
| | | |
| | | <!-- ä¸ãåºç¡ç¨éç»è®¡ --> |
| | | <div class="section-title">åºç¡ç¨éç»è®¡</div> |
| | | <el-row :gutter="12" class="summary-row"> |
| | | <el-col :span="4"> |
| | | <div class="summary-card total"> |
| | | <div class="label">å¨æç´¯è®¡çµé</div> |
| | | <div class="value">{{ formatKwh(analytics.totalConsumption) }} <span>kWh</span></div> |
| | | </div> |
| | | </el-col> |
| | | <el-col :span="5"> |
| | | <div class="summary-card"> |
| | | <div class="label">å°æ¶å¹³åç¨çµé</div> |
| | | <div class="value">{{ formatKwh(analytics.avgConsumption) }} <span>kWh</span></div> |
| | | </div> |
| | | </el-col> |
| | | <el-col :span="5"> |
| | | <div class="summary-card"> |
| | | <div class="label">å°æ¶æå¤§ç¨çµé</div> |
| | | <div class="value">{{ formatKwh(analytics.maxConsumption) }} <span>kWh</span></div> |
| | | </div> |
| | | </el-col> |
| | | <el-col :span="5"> |
| | | <div class="summary-card"> |
| | | <div class="label">å°æ¶æå°ç¨çµé</div> |
| | | <div class="value">{{ formatKwh(analytics.minConsumption) }} <span>kWh</span></div> |
| | | </div> |
| | | </el-col> |
| | | <el-col :span="5"> |
| | | <div class="summary-card load"> |
| | | <div class="label">è´è·ç</div> |
| | | <div class="value">{{ formatKwh(analytics.loadRate, 1) }} <span>%</span></div> |
| | | <div class="hint">å¹³å÷æå¤§Ã100</div> |
| | | </div> |
| | | </el-col> |
| | | </el-row> |
| | | |
| | | <!-- äºãåæ¯ç¯æ¯ --> |
| | | <div class="section-title">忝 / ç¯æ¯åæ</div> |
| | | <el-row :gutter="16" class="compare-row"> |
| | | <el-col :span="12"> |
| | | <div class="compare-card"> |
| | | <div class="compare-label">{{ analytics.chainComparison?.label || "ç¯æ¯ä¸æ" }}</div> |
| | | <div class="compare-body"> |
| | | <div> |
| | | <span class="sub">䏿</span> |
| | | <strong>{{ formatKwh(analytics.chainComparison?.compareTotal) }} kWh</strong> |
| | | </div> |
| | | <div> |
| | | <span class="sub">æ¬æ</span> |
| | | <strong>{{ formatKwh(analytics.chainComparison?.currentTotal) }} kWh</strong> |
| | | </div> |
| | | <div :class="deltaClass(analytics.chainComparison?.delta)"> |
| | | {{ formatDelta(analytics.chainComparison) }} |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <div class="compare-card"> |
| | | <div class="compare-label">{{ analytics.yoyComparison?.label || "忝å»å¹´åæ" }}</div> |
| | | <div class="compare-body"> |
| | | <div> |
| | | <span class="sub">å»å¹´åæ</span> |
| | | <strong>{{ formatKwh(analytics.yoyComparison?.compareTotal) }} kWh</strong> |
| | | </div> |
| | | <div> |
| | | <span class="sub">æ¬æ</span> |
| | | <strong>{{ formatKwh(analytics.yoyComparison?.currentTotal) }} kWh</strong> |
| | | </div> |
| | | <div :class="deltaClass(analytics.yoyComparison?.delta)"> |
| | | {{ formatDelta(analytics.yoyComparison) }} |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </el-col> |
| | | </el-row> |
| | | |
| | | <!-- ä¸ãè¶å¿ä¸æå --> |
| | | <div class="section-title">è¶å¿ä¸æ¶æ®µåæ</div> |
| | | <el-row :gutter="16"> |
| | | <el-col :span="14"> |
| | | <div class="chart-panel"> |
| | | <div class="chart-toolbar"> |
| | | <span>ç¨çµè¶å¿ï¼{{ trendGranularityLabel }}ï¼</span> |
| | | <el-radio-group |
| | | v-if="!isSingleDay" |
| | | v-model="chartType" |
| | | size="small" |
| | | @change="renderAllCharts" |
| | | > |
| | | <el-radio-button value="line">æçº¿</el-radio-button> |
| | | <el-radio-button value="bar">æ±ç¶</el-radio-button> |
| | | </el-radio-group> |
| | | </div> |
| | | <div ref="trendChartRef" class="chart-container" /> |
| | | </div> |
| | | </el-col> |
| | | <el-col :span="10"> |
| | | <div class="chart-panel"> |
| | | <div class="chart-toolbar"><span>æ¶æ®µæåï¼å³°å¹³è°·ï¼</span></div> |
| | | <div ref="periodChartRef" class="chart-container short" /> |
| | | </div> |
| | | </el-col> |
| | | </el-row> |
| | | |
| | | <el-row :gutter="16" class="sub-chart-row"> |
| | | <el-col :span="8"> |
| | | <div class="chart-panel"> |
| | | <div class="chart-toolbar"><span>çæ¬¡ç¨çµå¯¹æ¯</span></div> |
| | | <div ref="shiftChartRef" class="chart-container short" /> |
| | | </div> |
| | | </el-col> |
| | | <el-col :span="8"> |
| | | <div class="chart-panel"> |
| | | <div class="chart-toolbar"><span>工使¥ / 伿¯æ¥</span></div> |
| | | <div ref="dayTypeChartRef" class="chart-container short" /> |
| | | </div> |
| | | </el-col> |
| | | <el-col :span="8"> |
| | | <div class="split-table-panel"> |
| | | <div class="chart-toolbar"><span>æåå æ¯æç»</span></div> |
| | | <el-table :data="splitTableRows" size="small" border max-height="280"> |
| | | <el-table-column prop="category" label="ç±»å«" width="80" /> |
| | | <el-table-column prop="name" label="项" min-width="80" /> |
| | | <el-table-column label="çµé(kWh)" width="100"> |
| | | <template #default="{ row }">{{ formatKwh(row.consumption) }}</template> |
| | | </el-table-column> |
| | | <el-table-column label="å æ¯" width="70"> |
| | | <template #default="{ row }">{{ formatKwh(row.ratio, 1) }}%</template> |
| | | </el-table-column> |
| | | </el-table> |
| | | </div> |
| | | </el-col> |
| | | </el-row> |
| | | |
| | | <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 label="çµè¡¨" min-width="160" show-overflow-tooltip> |
| | | <template #default="{ row }"> |
| | | <div class="meter-cell"> |
| | | <span class="meter-name">{{ row.meterName || row.address || "-" }}</span> |
| | | <span v-if="row.meterId" class="meter-id">ID: {{ row.meterId }}</span> |
| | | </div> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="æ»çµé(kWh)" width="120"> |
| | | <template #default="{ row }">{{ formatKwh(row.totalConsumption) }}</template> |
| | | </el-table-column> |
| | | <el-table-column v-if="hasPeriodData" label="å³°(kWh)" width="90"> |
| | | <template #default="{ row }">{{ formatKwh(row.peakConsumption) }}</template> |
| | | </el-table-column> |
| | | <el-table-column v-if="hasPeriodData" label="å¹³(kWh)" width="90"> |
| | | <template #default="{ row }">{{ formatKwh(row.flatConsumption) }}</template> |
| | | </el-table-column> |
| | | <el-table-column v-if="hasPeriodData" label="è°·(kWh)" width="90"> |
| | | <template #default="{ row }">{{ formatKwh(row.valleyConsumption) }}</template> |
| | | </el-table-column> |
| | | <el-table-column label="æä½" width="80" fixed="right"> |
| | | <template #default="{ row }"> |
| | | <el-button type="primary" link @click="showDetail(row)">详æ
</el-button> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | </el-card> |
| | | |
| | | <el-dialog v-model="detailVisible" title="ç¨çµæç»è¯¦æ
" width="520px" destroy-on-close> |
| | | <el-descriptions v-if="detailRow" :column="1" border size="small"> |
| | | <el-descriptions-item label="æ¶é´"> |
| | | {{ parseTimeKey(detailRow.timeKey, queryForm.dimension) }} |
| | | </el-descriptions-item> |
| | | <el-descriptions-item label="çµè¡¨åç§°">{{ detailRow.meterName || "-" }}</el-descriptions-item> |
| | | <el-descriptions-item label="çµè¡¨ID">{{ detailRow.meterId ?? "-" }}</el-descriptions-item> |
| | | <el-descriptions-item label="表å°å">{{ detailRow.address || "-" }}</el-descriptions-item> |
| | | <el-descriptions-item label="æ»çµé(kWh)">{{ formatKwh(detailRow.totalConsumption) }}</el-descriptions-item> |
| | | <el-descriptions-item label="å¼å§æ¶é´">{{ detailRow.startTime || "-" }}</el-descriptions-item> |
| | | <el-descriptions-item label="ç»ææ¶é´">{{ detailRow.endTime || "-" }}</el-descriptions-item> |
| | | </el-descriptions> |
| | | </el-dialog> |
| | | </div> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { computed, getCurrentInstance, nextTick, onBeforeUnmount, onMounted, reactive, ref, watch } from "vue"; |
| | | import { ElMessageBox } from "element-plus"; |
| | | import * as echarts from "echarts"; |
| | | import { |
| | | analyticsStatisticEle, |
| | | formatDayPicker, |
| | | formatMonthTime, |
| | | getYesterdayDayPicker, |
| | | parseTimeKey, |
| | | } from "@/api/energyManagement/statisticEle.js"; |
| | | |
| | | const { proxy } = getCurrentInstance(); |
| | | |
| | | const loading = ref(false); |
| | | const trendChartRef = ref(null); |
| | | const periodChartRef = ref(null); |
| | | const shiftChartRef = ref(null); |
| | | const dayTypeChartRef = ref(null); |
| | | const chartInstances = {}; |
| | | |
| | | const queryForm = reactive({ dimension: "day", trendGranularity: "hour" }); |
| | | const chartType = ref("line"); |
| | | const analytics = ref({}); |
| | | const detailRecords = ref([]); |
| | | const detailVisible = ref(false); |
| | | const detailRow = ref(null); |
| | | |
| | | const dayRange = ref([]); |
| | | const monthRange = ref([]); |
| | | const quarterRange = ref([]); |
| | | const yearRange = ref([]); |
| | | |
| | | const isSingleDay = computed(() => { |
| | | if (!["day", "week"].includes(queryForm.dimension) || !dayRange.value?.length) return false; |
| | | return dayRange.value[0] === dayRange.value[1]; |
| | | }); |
| | | |
| | | const trendGranularityLabel = computed(() => { |
| | | const map = { hour: "å°æ¶", day: "æ¥", week: "å¨", month: "æ", year: "å¹´" }; |
| | | return map[analytics.value.trendGranularity || queryForm.trendGranularity] || "æ¥"; |
| | | }); |
| | | |
| | | const hasPeriodData = computed(() => |
| | | (analytics.value.periodSplits || []).length > 0 |
| | | || detailRecords.value.some((row) => |
| | | hasPeriodValue(row, "peakConsumption") |
| | | || hasPeriodValue(row, "flatConsumption") |
| | | || hasPeriodValue(row, "valleyConsumption") |
| | | ) |
| | | ); |
| | | |
| | | const splitTableRows = computed(() => { |
| | | const rows = []; |
| | | const push = (category, list) => { |
| | | (list || []).forEach((item) => rows.push({ category, ...item })); |
| | | }; |
| | | push("峰平谷", analytics.value.periodSplits); |
| | | push("çæ¬¡", analytics.value.shiftSplits); |
| | | push("æ¥ç±»å", analytics.value.dayTypeSplits); |
| | | return rows; |
| | | }); |
| | | |
| | | const dayShortcuts = [ |
| | | { |
| | | text: "æ¨æ¥", |
| | | value: () => { |
| | | const d = new Date(); |
| | | d.setDate(d.getDate() - 1); |
| | | return [d, d]; |
| | | }, |
| | | }, |
| | | { |
| | | text: "è¿7天", |
| | | value: () => { |
| | | const end = new Date(); |
| | | const start = new Date(end.getTime() - 6 * 86400000); |
| | | return [start, end]; |
| | | }, |
| | | }, |
| | | { |
| | | text: "è¿30天", |
| | | value: () => { |
| | | const end = new Date(); |
| | | const start = new Date(end.getTime() - 29 * 86400000); |
| | | return [start, end]; |
| | | }, |
| | | }, |
| | | ]; |
| | | |
| | | const PIE_COLORS = ["#409EFF", "#67C23A", "#E6A23C", "#F56C6C", "#909399"]; |
| | | |
| | | function hasPeriodValue(row, field) { |
| | | const n = Number(row?.[field]); |
| | | return Number.isFinite(n) && n !== 0; |
| | | } |
| | | |
| | | function formatKwh(value, digits = 2) { |
| | | const n = Number(value); |
| | | if (!Number.isFinite(n)) return (0).toFixed(digits); |
| | | return n.toFixed(digits); |
| | | } |
| | | |
| | | function formatDelta(comp) { |
| | | if (!comp) return "-"; |
| | | const sign = comp.delta >= 0 ? "+" : ""; |
| | | return `${sign}${formatKwh(comp.delta)} kWh (${sign}${formatKwh(comp.changeRate, 1)}%)`; |
| | | } |
| | | |
| | | function deltaClass(delta) { |
| | | if (delta > 0) return "delta up"; |
| | | if (delta < 0) return "delta down"; |
| | | return "delta"; |
| | | } |
| | | |
| | | 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" || dim === "week") { |
| | | 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] }; |
| | | } |
| | | |
| | | function syncTrendGranularity() { |
| | | if (isSingleDay.value) { |
| | | queryForm.trendGranularity = "hour"; |
| | | chartType.value = "line"; |
| | | return; |
| | | } |
| | | if (queryForm.trendGranularity === "hour") { |
| | | queryForm.trendGranularity = "day"; |
| | | } |
| | | chartType.value = "bar"; |
| | | } |
| | | |
| | | async function handleQuery() { |
| | | syncTrendGranularity(); |
| | | loading.value = true; |
| | | try { |
| | | const params = { |
| | | dimension: queryForm.dimension, |
| | | trendGranularity: queryForm.trendGranularity, |
| | | ...buildTimeParams(), |
| | | }; |
| | | const res = await analyticsStatisticEle(params); |
| | | analytics.value = res.data || {}; |
| | | detailRecords.value = res.data?.records || []; |
| | | await nextTick(); |
| | | renderAllCharts(); |
| | | } finally { |
| | | loading.value = false; |
| | | } |
| | | } |
| | | |
| | | function getChart(key, refEl) { |
| | | if (!refEl) return null; |
| | | if (!chartInstances[key]) { |
| | | chartInstances[key] = echarts.init(refEl); |
| | | } |
| | | return chartInstances[key]; |
| | | } |
| | | |
| | | function renderTrendChart() { |
| | | const chart = getChart("trend", trendChartRef.value); |
| | | if (!chart) return; |
| | | const gran = analytics.value.trendGranularity || queryForm.trendGranularity; |
| | | const records = analytics.value.trendRecords || analytics.value.chartRecords || []; |
| | | const labels = records.map((item) => parseTimeKey(item.timeKey, gran)); |
| | | const values = records.map((item) => Number(formatKwh(item.totalConsumption))); |
| | | const type = isSingleDay.value ? "line" : chartType.value; |
| | | chart.setOption({ |
| | | tooltip: { trigger: "axis" }, |
| | | grid: { left: 50, right: 20, top: 30, bottom: 50 }, |
| | | xAxis: { type: "category", data: labels, axisLabel: { rotate: gran === "hour" ? 45 : 30, fontSize: 11 } }, |
| | | yAxis: { type: "value", name: "kWh" }, |
| | | series: [{ |
| | | name: "ç¨çµé", |
| | | type, |
| | | data: values, |
| | | smooth: type === "line", |
| | | areaStyle: type === "line" ? { opacity: 0.1 } : undefined, |
| | | itemStyle: { color: "#409EFF" }, |
| | | barMaxWidth: 36, |
| | | }], |
| | | }, true); |
| | | } |
| | | |
| | | function renderPieChart(key, refEl, items, title) { |
| | | const chart = getChart(key, refEl); |
| | | if (!chart) return; |
| | | const data = (items || []).map((item, i) => ({ |
| | | name: item.name, |
| | | value: Number(formatKwh(item.consumption)), |
| | | itemStyle: { color: PIE_COLORS[i % PIE_COLORS.length] }, |
| | | })); |
| | | chart.setOption({ |
| | | title: data.length ? undefined : { text: "ææ æ°æ®", left: "center", top: "center", textStyle: { color: "#909399", fontSize: 13 } }, |
| | | tooltip: { trigger: "item", formatter: "{b}: {c} kWh ({d}%)" }, |
| | | legend: { bottom: 0, type: "scroll" }, |
| | | series: [{ |
| | | name: title, |
| | | type: "pie", |
| | | radius: ["40%", "65%"], |
| | | center: ["50%", "45%"], |
| | | data, |
| | | label: { formatter: "{b}\n{d}%" }, |
| | | }], |
| | | }, true); |
| | | } |
| | | |
| | | function renderBarChart(key, refEl, items, title) { |
| | | const chart = getChart(key, refEl); |
| | | if (!chart) return; |
| | | const list = items || []; |
| | | chart.setOption({ |
| | | title: list.length ? undefined : { text: "ææ æ°æ®", left: "center", top: "center", textStyle: { color: "#909399", fontSize: 13 } }, |
| | | tooltip: { trigger: "axis" }, |
| | | grid: { left: 50, right: 16, top: 20, bottom: 30 }, |
| | | xAxis: { type: "category", data: list.map((i) => i.name) }, |
| | | yAxis: { type: "value", name: "kWh" }, |
| | | series: [{ |
| | | name: title, |
| | | type: "bar", |
| | | data: list.map((i) => Number(formatKwh(i.consumption))), |
| | | itemStyle: { color: "#409EFF" }, |
| | | barMaxWidth: 40, |
| | | }], |
| | | }, true); |
| | | } |
| | | |
| | | function renderAllCharts() { |
| | | renderTrendChart(); |
| | | renderPieChart("period", periodChartRef.value, analytics.value.periodSplits, "峰平谷"); |
| | | renderBarChart("shift", shiftChartRef.value, analytics.value.shiftSplits, "çæ¬¡"); |
| | | renderPieChart("dayType", dayTypeChartRef.value, analytics.value.dayTypeSplits, "æ¥ç±»å"); |
| | | } |
| | | |
| | | function handleDimensionChange() { |
| | | syncTrendGranularity(); |
| | | handleQuery(); |
| | | } |
| | | |
| | | function showDetail(row) { |
| | | detailRow.value = row; |
| | | detailVisible.value = true; |
| | | } |
| | | |
| | | function handleExport() { |
| | | ElMessageBox.confirm("确认导åºå½åç»è®¡æ¥è¡¨ï¼", "导åº", { type: "warning" }) |
| | | .then(() => { |
| | | proxy.download("/statisticEle/export", { |
| | | dimension: queryForm.dimension, |
| | | ...buildTimeParams(), |
| | | }, `è½èç»è®¡_${queryForm.dimension}.xlsx`); |
| | | }) |
| | | .catch(() => {}); |
| | | } |
| | | |
| | | function handleResize() { |
| | | Object.values(chartInstances).forEach((c) => c?.resize()); |
| | | } |
| | | |
| | | watch(isSingleDay, () => { |
| | | syncTrendGranularity(); |
| | | renderAllCharts(); |
| | | }); |
| | | |
| | | onMounted(() => { |
| | | initDefaultRange(); |
| | | handleQuery(); |
| | | window.addEventListener("resize", handleResize); |
| | | }); |
| | | |
| | | onBeforeUnmount(() => { |
| | | window.removeEventListener("resize", handleResize); |
| | | Object.values(chartInstances).forEach((c) => c?.dispose()); |
| | | }); |
| | | </script> |
| | | |
| | | <style scoped> |
| | | .card-header { display: flex; align-items: center; gap: 12px; } |
| | | .card-header .desc { font-size: 13px; color: #909399; } |
| | | .search-form { margin-bottom: 12px; } |
| | | .time-range-item { margin-right: 0; } |
| | | .section-title { |
| | | font-weight: 600; |
| | | font-size: 14px; |
| | | margin: 16px 0 10px; |
| | | padding-left: 8px; |
| | | border-left: 3px solid #409eff; |
| | | } |
| | | .summary-row { margin-bottom: 8px; } |
| | | .summary-card { |
| | | background: #f5f7fa; |
| | | border-radius: 8px; |
| | | padding: 16px 12px; |
| | | text-align: center; |
| | | min-height: 96px; |
| | | } |
| | | .summary-card.total { background: linear-gradient(135deg, #409eff22, #409eff11); } |
| | | .summary-card.load { background: linear-gradient(135deg, #67c23a22, #67c23a11); } |
| | | .summary-card .label { font-size: 12px; color: #909399; margin-bottom: 6px; } |
| | | .summary-card .value { font-size: 22px; font-weight: 600; } |
| | | .summary-card .value span { font-size: 12px; font-weight: 400; color: #909399; } |
| | | .summary-card .hint { font-size: 11px; color: #c0c4cc; margin-top: 4px; } |
| | | .compare-row { margin-bottom: 8px; } |
| | | .compare-card { |
| | | background: #fafafa; |
| | | border: 1px solid #ebeef5; |
| | | border-radius: 8px; |
| | | padding: 14px 16px; |
| | | } |
| | | .compare-label { font-weight: 500; margin-bottom: 10px; } |
| | | .compare-body { display: flex; justify-content: space-between; align-items: center; gap: 12px; flex-wrap: wrap; } |
| | | .compare-body .sub { display: block; font-size: 12px; color: #909399; margin-bottom: 2px; } |
| | | .delta { font-weight: 600; font-size: 13px; } |
| | | .delta.up { color: #f56c6c; } |
| | | .delta.down { color: #67c23a; } |
| | | .chart-panel, .split-table-panel { |
| | | border: 1px solid #ebeef5; |
| | | border-radius: 8px; |
| | | padding: 12px; |
| | | margin-bottom: 12px; |
| | | } |
| | | .sub-chart-row { margin-top: 0; } |
| | | .chart-toolbar { |
| | | display: flex; |
| | | justify-content: space-between; |
| | | align-items: center; |
| | | margin-bottom: 8px; |
| | | font-weight: 500; |
| | | font-size: 13px; |
| | | } |
| | | .chart-container { width: 100%; height: 320px; } |
| | | .chart-container.short { height: 280px; } |
| | | .detail-title { font-weight: 500; margin: 8px 0 10px; } |
| | | .meter-cell { display: flex; flex-direction: column; gap: 2px; } |
| | | .meter-name { font-size: 13px; } |
| | | .meter-id { font-size: 12px; color: #909399; } |
| | | </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> |
| | |
| | | </el-select> |
| | | </el-form-item> |
| | | </el-col> |
| | | <!-- <el-col :span="12"> |
| | | <el-col :span="12"> |
| | | <el-form-item label="æ¯å¦ç产ï¼" |
| | | prop="isProduction"> |
| | | <el-radio-group v-model="productForm.isProduction"> |
| | |
| | | :value="false" /> |
| | | </el-radio-group> |
| | | </el-form-item> |
| | | </el-col>--> |
| | | </el-col> |
| | | </el-row> |
| | | </el-form> |
| | | </FormDialog> |
| | |
| | | taxInclusiveTotalPrice: taxInclusiveTotalPrice, |
| | | taxExclusiveTotalPrice: taxExclusiveTotalPrice, |
| | | invoiceType: "墿®ç¥¨", |
| | | isProduction: true, |
| | | isProduction: false, |
| | | productId: p.productId, |
| | | productModelId: p.productModelId, |
| | | }; |
| | |
| | | productOperationType.value = type; |
| | | productForm.value = {}; |
| | | if (type === "add") { |
| | | productForm.value.isProduction = true; |
| | | productForm.value.isProduction = false; |
| | | } |
| | | proxy.resetForm("productFormRef"); |
| | | if (type === "edit") { |