用電即時分佈 (kwh) 取資料

This commit is contained in:
huliang 2025-04-22 18:08:37 +08:00
parent ab007abd72
commit affc5291f4
12 changed files with 436 additions and 53 deletions

View File

@ -5,6 +5,11 @@
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>EMS</title>
<script type="text/javascript" src="/requirejs/config.js?"></script>
<script
type="text/javascript"
src="/module/js/com/tridium/js/ext/require/require.min.js?"
></script>
</head>
<body>
<div id="app"></div>

127
package-lock.json generated
View File

@ -8,8 +8,10 @@
"name": "ems_front",
"version": "0.0.0",
"dependencies": {
"dayjs": "^1.11.13",
"echarts": "^5.6.0",
"element-plus": "^2.9.6",
"pinia": "^3.0.2",
"vue": "^3.5.13",
"vue-router": "^4.5.0"
},
@ -969,6 +971,28 @@
"integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==",
"license": "MIT"
},
"node_modules/@vue/devtools-kit": {
"version": "7.7.5",
"resolved": "https://registry.npmjs.org/@vue/devtools-kit/-/devtools-kit-7.7.5.tgz",
"integrity": "sha512-S9VAVJYVAe4RPx2JZb9ZTEi0lqTySz2CBeF0wHT5D3dkTLnT9yMMGegKNl4b2EIELwLSkcI9bl2qp0/jW+upqA==",
"dependencies": {
"@vue/devtools-shared": "^7.7.5",
"birpc": "^2.3.0",
"hookable": "^5.5.3",
"mitt": "^3.0.1",
"perfect-debounce": "^1.0.0",
"speakingurl": "^14.0.1",
"superjson": "^2.2.2"
}
},
"node_modules/@vue/devtools-shared": {
"version": "7.7.5",
"resolved": "https://registry.npmjs.org/@vue/devtools-shared/-/devtools-shared-7.7.5.tgz",
"integrity": "sha512-QBjG72RfpM0DKtpns2RZOxBltO226kOAls9e4Lri6YxS2gWTgL0H+wj1R2K76lxxIeOrqo4+2Ty6RQnzv+WSTQ==",
"dependencies": {
"rfdc": "^1.4.1"
}
},
"node_modules/@vue/language-core": {
"version": "2.2.8",
"resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-2.2.8.tgz",
@ -1224,6 +1248,14 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/birpc": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/birpc/-/birpc-2.3.0.tgz",
"integrity": "sha512-ijbtkn/F3Pvzb6jHypHRyve2QApOCZDR25D/VnkY2G/lBNcXCTsnsCxgY4k4PkVB7zfwzYbY3O9Lcqe3xufS5g==",
"funding": {
"url": "https://github.com/sponsors/antfu"
}
},
"node_modules/brace-expansion": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
@ -1279,6 +1311,20 @@
"dev": true,
"license": "MIT"
},
"node_modules/copy-anything": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-3.0.5.tgz",
"integrity": "sha512-yCEafptTtb4bk7GLEQoM8KVJpxAfdBJYaXyzQEgQQQgYrZiDp8SJmGKlYza6CYjEDNstAdNdKA3UuoULlEbS6w==",
"dependencies": {
"is-what": "^4.1.8"
},
"engines": {
"node": ">=12.13"
},
"funding": {
"url": "https://github.com/sponsors/mesqueeb"
}
},
"node_modules/csstype": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
@ -1288,8 +1334,7 @@
"node_modules/dayjs": {
"version": "1.11.13",
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz",
"integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==",
"license": "MIT"
"integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg=="
},
"node_modules/de-indent": {
"version": "1.0.2",
@ -1503,6 +1548,11 @@
"he": "bin/he"
}
},
"node_modules/hookable": {
"version": "5.5.3",
"resolved": "https://registry.npmjs.org/hookable/-/hookable-5.5.3.tgz",
"integrity": "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ=="
},
"node_modules/is-binary-path": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
@ -1549,6 +1599,17 @@
"node": ">=0.12.0"
}
},
"node_modules/is-what": {
"version": "4.1.16",
"resolved": "https://registry.npmjs.org/is-what/-/is-what-4.1.16.tgz",
"integrity": "sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A==",
"engines": {
"node": ">=12.13"
},
"funding": {
"url": "https://github.com/sponsors/mesqueeb"
}
},
"node_modules/js-tokens": {
"version": "9.0.1",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz",
@ -1628,6 +1689,11 @@
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/mitt": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz",
"integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw=="
},
"node_modules/mlly": {
"version": "1.7.4",
"resolved": "https://registry.npmjs.org/mlly/-/mlly-1.7.4.tgz",
@ -1722,6 +1788,11 @@
"dev": true,
"license": "MIT"
},
"node_modules/perfect-debounce": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz",
"integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA=="
},
"node_modules/picocolors": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
@ -1741,6 +1812,34 @@
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/pinia": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/pinia/-/pinia-3.0.2.tgz",
"integrity": "sha512-sH2JK3wNY809JOeiiURUR0wehJ9/gd9qFN2Y828jCbxEzKEmEt0pzCXwqiSTfuRsK9vQsOflSdnbdBOGrhtn+g==",
"dependencies": {
"@vue/devtools-api": "^7.7.2"
},
"funding": {
"url": "https://github.com/sponsors/posva"
},
"peerDependencies": {
"typescript": ">=4.4.4",
"vue": "^2.7.0 || ^3.5.11"
},
"peerDependenciesMeta": {
"typescript": {
"optional": true
}
}
},
"node_modules/pinia/node_modules/@vue/devtools-api": {
"version": "7.7.5",
"resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-7.7.5.tgz",
"integrity": "sha512-HYV3tJGARROq5nlVMJh5KKHk7GU8Au3IrrmNNqr978m0edxgpHgYPDoNUGrvEgIbObz09SQezFR3A1EVmB5WZg==",
"dependencies": {
"@vue/devtools-kit": "^7.7.5"
}
},
"node_modules/pkg-types": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.1.0.tgz",
@ -1824,6 +1923,11 @@
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/rfdc": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz",
"integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA=="
},
"node_modules/rollup": {
"version": "4.36.0",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.36.0.tgz",
@ -1879,6 +1983,14 @@
"node": ">=0.10.0"
}
},
"node_modules/speakingurl": {
"version": "14.0.1",
"resolved": "https://registry.npmjs.org/speakingurl/-/speakingurl-14.0.1.tgz",
"integrity": "sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/strip-literal": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.0.0.tgz",
@ -1892,6 +2004,17 @@
"url": "https://github.com/sponsors/antfu"
}
},
"node_modules/superjson": {
"version": "2.2.2",
"resolved": "https://registry.npmjs.org/superjson/-/superjson-2.2.2.tgz",
"integrity": "sha512-5JRxVqC8I8NuOUjzBbvVJAKNM8qoVuH0O77h4WInc/qC2q5IreqKxYwgkga3PfA22OayK2ikceb/B26dztPl+Q==",
"dependencies": {
"copy-anything": "^3.0.2"
},
"engines": {
"node": ">=16"
}
},
"node_modules/tinyglobby": {
"version": "0.2.12",
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.12.tgz",

View File

@ -9,8 +9,10 @@
"preview": "vite preview"
},
"dependencies": {
"dayjs": "^1.11.13",
"echarts": "^5.6.0",
"element-plus": "^2.9.6",
"pinia": "^3.0.2",
"vue": "^3.5.13",
"vue-router": "^4.5.0"
},

View File

@ -1,7 +1,9 @@
<script setup>
import { ref, onMounted, onUnmounted } from "vue";
import * as echarts from "echarts"; // echarts
import * as echarts from "echarts";
import useElecStore from "../stores/useElecDemandStore";
const store = useElecStore();
//
const data = {
categories: [
@ -82,22 +84,16 @@ const defaultChartOption = ref({
let chartInstance = null; //
onMounted(() => {
onMounted(async () => {
// await store.getElecDataFromBaja();
chartInstance = echarts.init(demand_chart.value);
chartInstance.setOption(defaultChartOption.value);
window.addEventListener("resize", () => {
chartInstance.resize();
});
});
onUnmounted(() => {
if (chartInstance) {
chartInstance.dispose();
}
window.removeEventListener("resize", () => {
chartInstance.resize();
});
});
</script>
<template>

View File

@ -1,16 +1,36 @@
<script setup>
import { ref, onMounted, onUnmounted } from "vue";
import { ref, onMounted, onUnmounted, watch } from "vue";
import * as echarts from "echarts";
import useElecStore from "../stores/useElecDistStore";
const store = useElecStore();
const chartContainer = ref(null);
let chart = null;
onMounted(() => {
initChart();
});
const initChart = () => {
chart = echarts.init(chartContainer.value);
};
const updateChart = (elecData) => {
if (!chart) {
return;
}
//
// const totalElectricity = elecData.reduce((sum, item) => sum + item.out, 0);
// data links
const chartData = [
{ name: "總用電", itemStyle: { color: "#038a77" } },
...elecData.map((item) => ({ name: item.displayName })),
];
const chartLinks = elecData.map((item) => ({
source: "總用電",
target: item.displayName,
value: Number(item.out),
}));
const option = {
tooltip: {
@ -24,22 +44,8 @@ const initChart = () => {
emphasis: {
focus: "adjacency",
},
data: [
{ name: "總用電", itemStyle: { color: '#038a77' } },
{ name: "照明" },
{ name: "空調" },
{ name: "設備A" },
{ name: "設備B" },
{ name: "其他" },
],
links: [
{ source: "總用電", target: "照明", value: 100 },
{ source: "總用電", target: "空調", value: 150 },
{ source: "總用電", target: "設備A", value: 80 },
{ source: "總用電", target: "設備B", value: 120 },
{ source: "總用電", target: "其他", value: 50 },
],
data: chartData,
links: chartLinks,
lineStyle: {
color: "gradient",
opacity: 0.7,
@ -52,22 +58,30 @@ const initChart = () => {
],
};
chart.setOption(option); //
// 調
window.addEventListener("resize", () => {
chart.resize();
});
chart.setOption(option);
};
//
watch(
() => store.elecData,
(newElecData) => {
if (newElecData && newElecData.length > 0) {
updateChart(newElecData);
}
},
{ deep: true, immediate: true }
);
onMounted(async () => {
await store.getElecDataFromBaja();
initChart();
store.startTimer();
});
onUnmounted(() => {
if (chart) {
chart.dispose();
chart.clear();
store.stopTimer();
}
window.removeEventListener("resize", () => {
chart.resize();
});
});
</script>
<template>

View File

@ -1,8 +1,11 @@
import { createApp } from 'vue'
import './reset.css'
import App from './App.vue'
import router from './router'
import { createApp } from "vue";
import "./reset.css";
import { createPinia } from "pinia";
import App from "./App.vue";
import router from "./router";
const app = createApp(App)
app.use(router)
app.mount('#app')
const pinia = createPinia();
const app = createApp(App);
app.use(pinia);
app.use(router);
app.mount("#app");

View File

@ -0,0 +1,44 @@
import { ref } from "vue";
import { defineStore } from "pinia";
// import dayjs from "dayjs";
import type { NiagaraElecData } from "../utils/types";
const useElecStore = defineStore("elecData", () => {
const elecData = ref<NiagaraElecData[]>([]);
// get data from baja
const getElecDataFromBaja = () => {
// @ts-ignore
window.require &&
// @ts-ignore
window.requirejs(["baja!"], (baja: any) => {
console.log("進入 bajaSubscriber 準備執行 BQL 訂閱");
let eleclist: NiagaraElecData[] = [];
baja.Ord.make(
`local:|foxs:4912|station:|neql:EMS:kw|bql:select slotPath,parent.displayName,name`
).get({
cursor: {
before: () => {
},
each: (record: any) => {
console.log("record", record);
// eleclist.push({
// slotPath: record.get("slotPath"),
// displayName: record.get("parent$2edisplayName"),
// id: record.get("NumericInterval$2ehistoryConfig$2eid").$cEncStr,
// out: record.get("out").get("value"),
// });
},
after: () => {
elecData.value = eleclist;
console.log("Niagara 用電:", elecData.value);
},
},
});
});
};
return { getElecDataFromBaja, elecData };
});
export default useElecStore;

View File

@ -0,0 +1,127 @@
import { ref } from "vue";
import { defineStore } from "pinia";
import dayjs from "dayjs";
import type { NiagaraElecData } from "../utils/types";
const useElecStore = defineStore("elecData", () => {
const elecData = ref<NiagaraElecData[]>([]);
// @ts-ignore
let timerId = null;
// get data from baja
const getElecDataFromBaja = () => {
// @ts-ignore
window.require &&
// @ts-ignore
window.requirejs(["baja!"], (baja: any) => {
console.log("進入 bajaSubscriber 準備執行 BQL 訂閱");
// 定義BQL 查詢
const subSysKwhBql = `local:|foxs:4918|station:|neql:EMS:SubSys_kwh|bql:select slotPath,parent.displayName,displayName,NumericInterval.historyConfig.id`;
// 執行各電表的 BQL 查詢
fetchElecData(baja, subSysKwhBql);
});
};
const fetchElecData = (baja: any, bql: string) => {
let eleclist: NiagaraElecData[] = [];
baja.Ord.make(bql).get({
cursor: {
before: () => {},
each: (record: any) => {
eleclist.push({
slotPath: record.get("slotPath"),
displayName: record.get("parent$2edisplayName"),
id: record.get("NumericInterval$2ehistoryConfig$2eid").$cEncStr,
out: 0,
});
},
after: () => {
const validElecList = eleclist.filter(
(item) => item.id !== undefined
);
elecData.value = [...elecData.value, ...validElecList];
validElecList.forEach((item) => {
subscribeToHistory(item);
});
},
},
});
};
const subscribeToHistory = (item: NiagaraElecData) => {
const startTime = dayjs()
.subtract(2, "hour")
.format("YYYY-MM-DDTHH:mm:ss.000+08:00"); // 現在時間前2個小時
const endTime = dayjs().format("YYYY-MM-DDTHH:mm:ss.000+08:00"); // 現在的時間
const id = item.id;
console.log(
`local:|foxs:4918|history:${id}?period=timerange;start=${startTime};end=${endTime}|bql:history:HistoryRollup.rollup(baja:RelTime '3600000')`
);
const ordString = `local:|foxs:4918|history:${id}?period=timerange;start=${startTime};end=${endTime}|bql:history:HistoryRollup.rollup(baja:RelTime '3600000')`;
// @ts-ignore
window.require &&
// @ts-ignore
window.requirejs(["baja!"], (baja: any) => {
console.log("進入 bajaSubscriber 準備執行 BQL 訂閱");
let historyData: number[] = [];
baja.Ord.make(ordString).get({
cursor: {
before: () => {
console.log(`開始訂閱 ${id} 的歷史資料`);
},
each: (record: any) => {
let currentValue = record.get("min");
historyData.push(currentValue);
},
after: () => {
const diff = historyData[historyData.length - 1] - historyData[0];
console.log(
`收到 ${id} 的歷史資料: 最後一筆減第一筆的值為:`,
diff
);
elecData.value = elecData.value.map((elec) => {
if (elec.id === id) {
console.log(`更新 ${id} 的 out 值為:`, diff);
return { ...elec, out: diff };
}
return elec;
});
},
},
});
});
};
// 定時更新資料的函數
const updateHistoryData = () => {
console.log("定時器觸發,重新獲取歷史資料");
if (elecData.value && elecData.value.length > 0) {
elecData.value.forEach((item) => {
subscribeToHistory(item);
});
}
};
// 啟動定時器
const startTimer = () => {
timerId = setInterval(() => {
updateHistoryData();
}, 60 * 1000); // 每小時執行一次
};
// 停止定時器
const stopTimer = () => {
// @ts-ignore
if (timerId) {
clearInterval(timerId);
timerId = null;
console.log("計時器已停止");
}
};
return { getElecDataFromBaja, startTimer, stopTimer, elecData };
});
export default useElecStore;

View File

@ -0,0 +1,59 @@
import { ref } from "vue";
import { defineStore } from "pinia";
// import dayjs from "dayjs";
import type { NiagaraElecData } from "../utils/types";
const useElecStore = defineStore("elecData", () => {
// @ts-ignore
let timer = null;
const elecData = ref<NiagaraElecData[]>([]);
// get data from baja
const getElecDataFromBaja = () => {
// @ts-ignore
window.require &&
// @ts-ignore
window.requirejs(["baja!"], (baja: any) => {
console.log("進入 bajaSubscriber 準備執行 BQL 訂閱");
let eleclist: NiagaraElecData[] = [];
baja.Ord.make(
`local:|foxs:4912|station:|neql:EMS:SubSys_kwh|bql:select slotPath,parent.displayName,name,out`
).get({
cursor: {
before: () => {
timer = null;
},
each: (record: any) => {
console.log("record", record);
eleclist.push({
slotPath: record.get("slotPath"),
displayName: record.get("parent$2edisplayName"),
id: record.get("NumericInterval$2ehistoryConfig$2eid").$cEncStr,
out: record.get("out").get("value"),
});
},
after: () => {
elecData.value = eleclist;
console.log("Niagara 用電:", elecData.value);
timer = setTimeout(() => {
getElecDataFromBaja();
}, 30000);
},
},
});
});
};
const clearElecDataFromBaja = () => {
// @ts-ignore
if (timer) {
clearTimeout(timer);
timer = null;
console.log("Timeout 已清除");
}
};
return { getElecDataFromBaja, clearElecDataFromBaja, elecData };
});
export default useElecStore;

7
src/utils/types.ts Normal file
View File

@ -0,0 +1,7 @@
// Niagara 用電數據類型定義
export interface NiagaraElecData {
slotPath: string;
displayName: string;
id: string;
out: number;
}

View File

@ -2,7 +2,7 @@
<el-row :gutter="20">
<el-col :span="10">
<el-card style="border-radius: 8px">
<h3 class="">用電即時分佈</h3>
<h3 class="">用電即時分佈 (kwh)</h3>
<EnergySankey />
</el-card>
</el-col>
@ -65,7 +65,7 @@
</template>
<script setup>
import { onMounted, ref, computed } from "vue";
import { onMounted, ref, computed, onUnmounted } from "vue";
import EnergySankey from "../components/EnergySankey.vue";
import EnergyLine from "../components/EnergyLine.vue";
import EnergyBar from "../components/EnergyBar.vue";
@ -208,6 +208,8 @@ const areaBillingData = ref({
},
],
});
</script>
<style scoped>

View File

@ -8,9 +8,10 @@ import { ElementPlusResolver } from "unplugin-vue-components/resolvers";
export default defineConfig({
base:
process.env.NODE_ENV === "production"
? "https://192.168.0.206:8500/file/ibms_ems_dist/"
? "https://192.168.0.206:8500/file/ems_dist/"
: "/",
build: {
outDir: process.env.NODE_ENV === "production" ? "../ems_dist" : "./dist",
emptyOutDir: true,
},
plugins: [