首頁串接資料與能源管理去掉sidebar menu
This commit is contained in:
parent
143a7ae061
commit
bc51a9db1f
@ -69,19 +69,14 @@ export const getDashboardFormulaRoom = async ({ timeInterval, typeOption }) => {
|
||||
export const getDashboardTemp = async ({
|
||||
timeInterval,
|
||||
tempOption,
|
||||
typeOption = "",
|
||||
building_guid,
|
||||
}) => {
|
||||
const res = typeOption
|
||||
? await instance.post(GET_DASHBOARD_TEMP_API, {
|
||||
timeInterval,
|
||||
tempOption,
|
||||
typeOption,
|
||||
})
|
||||
: await instance.post(GET_DASHBOARD_TEMP_API, {
|
||||
timeInterval,
|
||||
tempOption,
|
||||
});
|
||||
console.log(res);
|
||||
const res = await instance.post(GET_DASHBOARD_TEMP_API, {
|
||||
timeInterval,
|
||||
tempOption,
|
||||
building_guid,
|
||||
});
|
||||
|
||||
return apihandler(res.code, res.data, {
|
||||
msg: res.msg,
|
||||
code: res.code,
|
||||
|
@ -1,3 +1,7 @@
|
||||
export const GET_REALTIME_DATA_API = `/api/Energe/GetRealTimeData`;
|
||||
export const GET_ELEC_MONTH_API = `/api/Energe/GetElecUseMonth`;
|
||||
export const GET_ELEC_DAY_API = `/api/Energe/GetElecUseDay`;
|
||||
|
||||
export const GET_REALTIME_DIST_API = `/api/Energe/GetRealTimeDistribution`;
|
||||
export const GET_ELECUSE_DAY_API = `/api/Energe/GetElecUseDay`;
|
||||
export const GET_TAI_POWER_API = `/api/Energe/GetTaipower`;
|
||||
|
@ -1,4 +1,7 @@
|
||||
import {
|
||||
GET_REALTIME_DATA_API,
|
||||
GET_ELEC_MONTH_API,
|
||||
GET_ELEC_DAY_API,
|
||||
GET_REALTIME_DIST_API,
|
||||
GET_ELECUSE_DAY_API,
|
||||
GET_TAI_POWER_API,
|
||||
@ -18,6 +21,33 @@ import instance, { fileInstance } from "@/util/request";
|
||||
import apihandler from "@/util/apihandler";
|
||||
import downloadExcel from "@/util/downloadExcel";
|
||||
|
||||
export const getRealTimeData = async () => {
|
||||
const res = await instance.post(GET_REALTIME_DATA_API);
|
||||
|
||||
return apihandler(res.code, res.data, {
|
||||
msg: res.msg,
|
||||
code: res.code,
|
||||
});
|
||||
};
|
||||
|
||||
export const getElecUseMonth = async () => {
|
||||
const res = await instance.post(GET_ELEC_MONTH_API);
|
||||
|
||||
return apihandler(res.code, res.data, {
|
||||
msg: res.msg,
|
||||
code: res.code,
|
||||
});
|
||||
};
|
||||
|
||||
export const getElecUseofDay = async () => {
|
||||
const res = await instance.post(GET_ELEC_DAY_API);
|
||||
|
||||
return apihandler(res.code, res.data, {
|
||||
msg: res.msg,
|
||||
code: res.code,
|
||||
});
|
||||
};
|
||||
|
||||
export const getRealTimeDist = async ({
|
||||
building_guid,
|
||||
department_id_list,
|
||||
|
@ -18,15 +18,9 @@ export const getSystemFloors = async (building_tag, sub_system_tag) => {
|
||||
});
|
||||
};
|
||||
|
||||
export const getSystemDevices = async ({
|
||||
sub_system_tag,
|
||||
building_guid,
|
||||
department_id_list,
|
||||
}) => {
|
||||
export const getSystemDevices = async ({ building_guid }) => {
|
||||
const res = await instance.post(GET_SYSTEM_DEVICE_LIST_API, {
|
||||
sub_system_tag,
|
||||
building_guid,
|
||||
department_id_list,
|
||||
});
|
||||
|
||||
return apihandler(res.code, res.data, {
|
||||
|
@ -30,8 +30,8 @@ const open = ref(false);
|
||||
const showDrawer = async (authCode) => {
|
||||
if (authCode === "PF1") {
|
||||
emit("show-drawer", buildingStore.selectedBuilding.building_guid); // 使用 emit 呼叫父組件的函数
|
||||
} else if (authCode === "PF2" || authCode === "PF11") {
|
||||
emit("getSubPage", authCode === "PF2" ? "Energy" : "Setting");
|
||||
} else if (authCode === "PF11") {
|
||||
emit("getSubPage", "Setting");
|
||||
}
|
||||
currentAuthCode.value = authCode;
|
||||
open.value = true;
|
||||
@ -75,11 +75,7 @@ const handleOpenChange = (keys) => {
|
||||
:key="page.authCode"
|
||||
>
|
||||
<a
|
||||
v-if="
|
||||
page.authCode === 'PF1' ||
|
||||
page.authCode === 'PF2' ||
|
||||
page.authCode === 'PF11'
|
||||
"
|
||||
v-if="page.authCode === 'PF1' || page.authCode === 'PF11'"
|
||||
@click="showDrawer(page.authCode)"
|
||||
:class="
|
||||
twMerge(
|
||||
@ -87,10 +83,6 @@ const handleOpenChange = (keys) => {
|
||||
page.authCode === 'PF1' && route.fullPath.includes('/system')
|
||||
? 'router-link-active router-link-exact-active'
|
||||
: '',
|
||||
page.authCode === 'PF2' &&
|
||||
route.fullPath.includes('/energyManagement')
|
||||
? 'router-link-active router-link-exact-active'
|
||||
: '',
|
||||
page.authCode === 'PF11' && route.fullPath.includes('/setting')
|
||||
? 'router-link-active router-link-exact-active'
|
||||
: ''
|
||||
@ -144,24 +136,22 @@ const handleOpenChange = (keys) => {
|
||||
<a-menu-item
|
||||
v-for="sub in currentAuthCode === 'PF1'
|
||||
? main.history_Sub_systems
|
||||
: currentAuthCode === 'PF2'
|
||||
? main.sub
|
||||
: main.sub"
|
||||
:key="sub.sub_system_tag + `_` + sub.type"
|
||||
@click="() => {onClose(); closeDrawer();}"
|
||||
@click="
|
||||
() => {
|
||||
onClose();
|
||||
closeDrawer();
|
||||
}
|
||||
"
|
||||
>
|
||||
<router-link
|
||||
:to="{
|
||||
name:
|
||||
currentAuthCode === 'PF2'
|
||||
? 'energyManagement'
|
||||
: currentAuthCode === 'PF11'
|
||||
? 'setting'
|
||||
: 'sub_system',
|
||||
name: currentAuthCode === 'PF11' ? 'setting' : 'sub_system',
|
||||
params: {
|
||||
main_system_id: main.main_system_tag,
|
||||
sub_system_id: sub.sub_system_tag,
|
||||
...(currentAuthCode === 'PF2' || currentAuthCode === 'PF11'
|
||||
...(currentAuthCode === 'PF11'
|
||||
? { type: sub.type }
|
||||
: { floor_id: 'main' }),
|
||||
},
|
||||
@ -185,7 +175,7 @@ const handleOpenChange = (keys) => {
|
||||
}
|
||||
|
||||
.menu-box .btn-group span {
|
||||
@apply text-lg lg:text-sm ms-2 lg:ms-0 ;
|
||||
@apply text-lg lg:text-sm ms-2 lg:ms-0;
|
||||
}
|
||||
|
||||
.menu-box > li {
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { defineStore } from "pinia";
|
||||
import { ref, computed, watch } from "vue";
|
||||
import { useRoute } from "vue-router";
|
||||
import { getBuildings } from "@/apis/building";
|
||||
import { getBuildings, getAllSysSidebar } from "@/apis/building";
|
||||
import { getAssetFloorList, getDepartmentList } from "@/apis/asset";
|
||||
|
||||
const useBuildingStore = defineStore("buildingInfo", () => {
|
||||
@ -56,27 +56,39 @@ const useBuildingStore = defineStore("buildingInfo", () => {
|
||||
// 獲取樓層資料
|
||||
const fetchFloorList = async (building_guid) => {
|
||||
const res = await getAssetFloorList(building_guid);
|
||||
floorList.value = res.data[0]?.floors.map((d) => ({
|
||||
...d,
|
||||
title: d.full_name,
|
||||
key: d.floor_guid,
|
||||
})) || [];
|
||||
floorList.value =
|
||||
res.data[0]?.floors.map((d) => ({
|
||||
...d,
|
||||
title: d.full_name,
|
||||
key: d.floor_guid,
|
||||
})) || [];
|
||||
};
|
||||
|
||||
// 獲取部門資料
|
||||
const fetchDepartmentList = async () => {
|
||||
const res = await getDepartmentList();
|
||||
deptList.value = res.data.map((d) => ({
|
||||
...d,
|
||||
title: d.name,
|
||||
key: d.id,
|
||||
})) || [];
|
||||
deptList.value =
|
||||
res.data.map((d) => ({
|
||||
...d,
|
||||
title: d.name,
|
||||
key: d.id,
|
||||
})) || [];
|
||||
};
|
||||
|
||||
// 當 selectedBuilding 改變時,更新 floorList 和 deptList
|
||||
// 取得大小類
|
||||
const getSubMonitorPage = async (building_guid) => {
|
||||
const res = await getAllSysSidebar(building_guid);
|
||||
mainSubSys.value = res.data.history_Main_Systems;
|
||||
};
|
||||
|
||||
// 當 selectedBuilding 改變時,更新 floorList 和 deptList 和 mainSubSys
|
||||
watch(selectedBuilding, async (newBuilding) => {
|
||||
if (newBuilding) {
|
||||
await Promise.all([fetchFloorList(newBuilding.building_guid), fetchDepartmentList()]);
|
||||
await Promise.all([
|
||||
fetchFloorList(newBuilding.building_guid),
|
||||
fetchDepartmentList(),
|
||||
getSubMonitorPage(newBuilding.building_guid)
|
||||
]);
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -2,104 +2,79 @@
|
||||
import DashboardFloorBar from "./components/DashboardFloorBar.vue";
|
||||
import DashboardEffectScatter from "./components/DashboardEffectScatter.vue";
|
||||
import DashboardSysCard from "./components/DashboardSysCard.vue";
|
||||
import DashboardTemp from "./components/DashboardTemp.vue";
|
||||
import DashboardRefrigTemp from "./components/DashboardRefrigTemp.vue";
|
||||
import DashboardIndoorTemp from "./components/DashboardIndoorTemp.vue";
|
||||
import DashboardElectricity from "./components/DashboardElectricity.vue";
|
||||
import DashboardEmission from "./components/DashboardEmission.vue";
|
||||
import DashboardAlert from "./components/DashboardAlert.vue";
|
||||
import { computed, inject, ref, watch } from "vue";
|
||||
import useBuildingStore from "@/stores/useBuildingStore";
|
||||
import { getSystemDevices, getSystemRealTime } from "@/apis/system";
|
||||
const FILE_BASEURL = import.meta.env.VITE_FILE_API_BASEURL;
|
||||
const buildingStore = useBuildingStore()
|
||||
|
||||
const systemData = {
|
||||
'85e7d4b3-9570-4149-95d4-3739c00cee6a': [
|
||||
[
|
||||
120.1,
|
||||
480,
|
||||
{
|
||||
full_name: "空氣偵測器",
|
||||
state: "online",
|
||||
temperature: "25°C",
|
||||
icon: `${FILE_BASEURL}/upload/device_icon/3454f5e0-3afa-4ace-ae54-a68bf7183e7d.png`,
|
||||
bgColor: "rgba(100, 166, 182,0.5)",
|
||||
bgSize: 60,
|
||||
},
|
||||
],
|
||||
[
|
||||
580,
|
||||
800,
|
||||
{
|
||||
full_name: "空調",
|
||||
state: "offline",
|
||||
temperature: "-5°C",
|
||||
icon: `${FILE_BASEURL}/upload/device_icon/9d8e1dd3-8187-46e3-8a6a-ae116210ecff.png`,
|
||||
bgColor: "rgba(255, 0, 0,0.5)",
|
||||
bgSize: 60,
|
||||
},
|
||||
],
|
||||
[
|
||||
940,
|
||||
960,
|
||||
{
|
||||
full_name: "電錶",
|
||||
state: "online",
|
||||
temperature: "25°C",
|
||||
icon: `${FILE_BASEURL}/upload/device_icon/83deea51-a97e-4757-ba15-5cd94cb25929.png`,
|
||||
bgColor: "rgba(100, 166, 182,0.5)",
|
||||
bgSize: 60,
|
||||
},
|
||||
],
|
||||
],
|
||||
'5cf3c8da-a5b4-42da-8b1a-14d5a44a0456': [
|
||||
[
|
||||
280,
|
||||
280,
|
||||
{
|
||||
full_name: "空氣偵測器",
|
||||
state: "offline",
|
||||
temperature: "25°C",
|
||||
icon: `${FILE_BASEURL}/upload/device_icon/3454f5e0-3afa-4ace-ae54-a68bf7183e7d.png`,
|
||||
bgColor: "rgba(255, 0, 0,0.5)",
|
||||
bgSize: 60,
|
||||
},
|
||||
],
|
||||
[
|
||||
2800,
|
||||
3000,
|
||||
{
|
||||
full_name: "電錶",
|
||||
state: "online",
|
||||
temperature: "25°C",
|
||||
icon: `${FILE_BASEURL}/upload/device_icon/83deea51-a97e-4757-ba15-5cd94cb25929.png`,
|
||||
bgColor: "rgba(100, 166, 182,0.5)",
|
||||
bgSize: 60,
|
||||
},
|
||||
],
|
||||
[
|
||||
250,
|
||||
3000,
|
||||
{
|
||||
full_name: "電錶",
|
||||
state: "online",
|
||||
temperature: "25°C",
|
||||
icon: `${FILE_BASEURL}/upload/device_icon/83deea51-a97e-4757-ba15-5cd94cb25929.png`,
|
||||
bgColor: "rgba(100, 166, 182,0.5)",
|
||||
bgSize: 60,
|
||||
},
|
||||
],
|
||||
],
|
||||
'f5e5215b-d689-4d25-9c95-421964040bf8': [
|
||||
[
|
||||
940,
|
||||
960,
|
||||
{
|
||||
full_name: "電錶",
|
||||
state: "online",
|
||||
temperature: "25°C",
|
||||
icon: `${FILE_BASEURL}/upload/device_icon/83deea51-a97e-4757-ba15-5cd94cb25929.png`,
|
||||
bgColor: "rgba(100, 166, 182,0.5)",
|
||||
bgSize: 60,
|
||||
},
|
||||
],
|
||||
],
|
||||
};
|
||||
const subscribeData = ref([]);
|
||||
const systemData = ref({});
|
||||
const getData = async () => {
|
||||
const res = await getSystemDevices({
|
||||
building_guid: buildingStore.selectedBuilding?.building_guid,
|
||||
})
|
||||
|
||||
subscribeData.value = res.data
|
||||
console.log("devices", subscribeData.value)
|
||||
|
||||
// 轉換資料格式
|
||||
const transformedData = {};
|
||||
|
||||
subscribeData.value.forEach(floor => {
|
||||
if (floor.device_list && floor.device_list.length > 0) {
|
||||
const fullUrl = floor.floor_map_name;
|
||||
const uuid = fullUrl ? fullUrl.replace(/\.svg$/, "") : "";
|
||||
transformedData[uuid] = floor.device_list.map(device => {
|
||||
// 解析座標
|
||||
const coordinates = JSON.parse(device.device_coordinate || '[0,0]');
|
||||
const x = coordinates[0];
|
||||
const y = coordinates[1];
|
||||
|
||||
// 決定設備狀態和顏色
|
||||
let state = "online";
|
||||
let bgColor = "rgba(255, 255, 255)";
|
||||
|
||||
if (device.device_status === "offline" || device.device_status === null) {
|
||||
state = "offline";
|
||||
bgColor = "rgba(34, 51, 85)";
|
||||
}
|
||||
|
||||
return [
|
||||
x,
|
||||
y,
|
||||
{
|
||||
full_name: device.full_name,
|
||||
state: state,
|
||||
icon: device.device_image ? `${FILE_BASEURL}/upload/device_icon/${device.device_image}` : '',
|
||||
bgColor: bgColor,
|
||||
bgSize: 50,
|
||||
}
|
||||
];
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
console.log("transformedData", transformedData);
|
||||
systemData.value = transformedData;
|
||||
}
|
||||
|
||||
watch(
|
||||
() => buildingStore.selectedBuilding,
|
||||
(newBuilding) => {
|
||||
if (newBuilding) {
|
||||
getData();
|
||||
}
|
||||
},
|
||||
{
|
||||
immediate: true
|
||||
}
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@ -108,7 +83,10 @@ const systemData = {
|
||||
class="order-3 lg:order-1 w-full lg:w-1/4 h-full flex flex-col justify-start z-10 border-dashboard px-12"
|
||||
>
|
||||
<div>
|
||||
<DashboardTemp />
|
||||
<DashboardRefrigTemp />
|
||||
</div>
|
||||
<div class="mt-10">
|
||||
<DashboardIndoorTemp />
|
||||
</div>
|
||||
<div class="mt-10">
|
||||
<DashboardAlert />
|
||||
|
@ -12,7 +12,7 @@ const FILE_BASEURL = import.meta.env.VITE_FILE_API_BASEURL;
|
||||
const props = defineProps({
|
||||
data: {
|
||||
type: Object,
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const asset_floor_chart = ref(null);
|
||||
@ -32,15 +32,14 @@ const defaultOption = (map, data = []) => {
|
||||
<div class="text-lg">${params.data[2].full_name}</div>
|
||||
<div class="text-sm text-gray-500">狀態: ${
|
||||
params.data[2].state || ""
|
||||
}</div>
|
||||
<div class="text-sm text-gray-500">溫度: ${
|
||||
params.data[2].temperature || ""
|
||||
}</div></div>`
|
||||
}</div>`
|
||||
: "";
|
||||
},
|
||||
},
|
||||
map,
|
||||
roam: true,
|
||||
roam: true, // 允許縮放和平移
|
||||
layoutSize: window.innerWidth <= 768 ? "110%" : "75%",
|
||||
layoutCenter: ["50%", "50%"],
|
||||
scaleLimit: { min: 1, max: 2 },
|
||||
},
|
||||
series: [
|
||||
@ -49,7 +48,7 @@ const defaultOption = (map, data = []) => {
|
||||
type: "scatter",
|
||||
coordinateSystem: "geo",
|
||||
geoIndex: 0,
|
||||
symbolSize: (value) => value[2]?.bgSize || 40,
|
||||
symbolSize: (value) => value[2]?.bgSize || 50,
|
||||
symbol: "range",
|
||||
itemStyle: {
|
||||
color: (params) => params.data[2]?.bgColor || "rgba(0,0,0,0.8)",
|
||||
@ -65,11 +64,14 @@ const defaultOption = (map, data = []) => {
|
||||
tooltip: 2,
|
||||
},
|
||||
symbolSize: 30,
|
||||
itemStyle: {
|
||||
color: "rgba(225,225,225,0.8)",
|
||||
},
|
||||
symbol: (value, params) => {
|
||||
// 若有 icon 屬性就用 image,否則用圓點
|
||||
return params.data[2]?.icon
|
||||
? `image://${params.data[2].icon}`
|
||||
: "circle";
|
||||
: "roundRect";
|
||||
},
|
||||
data,
|
||||
label: {
|
||||
@ -78,7 +80,7 @@ const defaultOption = (map, data = []) => {
|
||||
formatter: (params) => params.data[2]?.full_name || "",
|
||||
color: "#333",
|
||||
fontSize: 14,
|
||||
backgroundColor: "rgba(255,255,255,0.7)",
|
||||
backgroundColor: "rgba(255,255,255)",
|
||||
padding: [4, 4],
|
||||
borderRadius: 3,
|
||||
},
|
||||
@ -89,19 +91,19 @@ const defaultOption = (map, data = []) => {
|
||||
};
|
||||
|
||||
const currentIconData = computed(() => {
|
||||
return props.data[searchParams.value.floor_id] || [];
|
||||
const data = props.data?.[searchParams.value.floor_id] || [];
|
||||
return data;
|
||||
});
|
||||
|
||||
watch(
|
||||
[searchParams, () => asset_floor_chart],
|
||||
([newValue, newChart], [oldValue]) => {
|
||||
if (newValue.floor_id && newChart.value) {
|
||||
asset_floor_chart.value.updateSvg(
|
||||
[searchParams, () => asset_floor_chart.value, () => props.data],
|
||||
([newValue, newChart, newData], [oldValue]) => {
|
||||
if (newValue.floor_id && newChart && Object.keys(newData || {}).length > 0) {
|
||||
newChart.updateSvg(
|
||||
{
|
||||
full_name: newValue.floor_id,
|
||||
path: `${FILE_BASEURL}/upload/floor_map/${newValue.floor_id}.svg`,
|
||||
},
|
||||
|
||||
defaultOption(newValue.floor_id, currentIconData.value)
|
||||
);
|
||||
}
|
||||
|
@ -1,31 +1,29 @@
|
||||
<script setup>
|
||||
import LineChart from "@/components/chart/LineChart.vue";
|
||||
import { SECOND_CHART_COLOR } from "@/constant";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { ref, computed, onMounted, defineProps, watch } from "vue";
|
||||
import { ref, onMounted, watch, onUnmounted } from "vue";
|
||||
import DashboardElectricityModal from "./DashboardElectricityModal.vue";
|
||||
import { getDemand, getRealTimeDemand } from "@/apis/energy";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import useBuildingStore from "@/stores/useBuildingStore";
|
||||
import dayjs from "dayjs";
|
||||
import { faker } from '@faker-js/faker'; // 引入 faker.js
|
||||
|
||||
const weeks = ["週日", "週一", "週二", "週三", "週四", "週五", "週六"];
|
||||
const store = useBuildingStore();
|
||||
const { t } = useI18n();
|
||||
|
||||
const props = defineProps({
|
||||
data: Array,
|
||||
});
|
||||
|
||||
const electricity_chart = ref(null);
|
||||
const defaultChartOption = {
|
||||
tooltip: {
|
||||
trigger: "axis",
|
||||
},
|
||||
const demandData = ref(null);
|
||||
const realTimeDemand = ref([]);
|
||||
const demand_chart = ref(null);
|
||||
const intervalId = ref(null);
|
||||
// 預設為空,避免 `null` 造成 `ECharts` 失敗
|
||||
const defaultChartOption = ref({
|
||||
tooltip: { trigger: "axis" },
|
||||
legend: {
|
||||
data: [],
|
||||
textStyle: {
|
||||
color: "#ffffff",
|
||||
fontSize: 16,
|
||||
},
|
||||
textStyle: { color: "#ffffff", fontSize: 16 },
|
||||
top: "0%",
|
||||
},
|
||||
grid: {
|
||||
top: "10%",
|
||||
top: "20%",
|
||||
left: "0%",
|
||||
right: "0%",
|
||||
bottom: "0%",
|
||||
@ -33,85 +31,147 @@ const defaultChartOption = {
|
||||
},
|
||||
xAxis: {
|
||||
type: "category",
|
||||
splitLine: {
|
||||
show: false,
|
||||
},
|
||||
axisLabel: {
|
||||
color: "#ffffff",
|
||||
},
|
||||
data: weeks,
|
||||
splitLine: { show: false },
|
||||
axisLabel: { color: "#ffffff" },
|
||||
data: [], // 預設空值,避免 undefined
|
||||
},
|
||||
yAxis: {
|
||||
type: "value",
|
||||
splitLine: {
|
||||
show: false,
|
||||
},
|
||||
axisLabel: {
|
||||
color: "#ffffff",
|
||||
},
|
||||
splitLine: { show: false },
|
||||
axisLabel: { color: "#ffffff" },
|
||||
},
|
||||
series: [],
|
||||
};
|
||||
});
|
||||
|
||||
const generateFakeData = () => {
|
||||
const seriesNames = ["本週", "上週"]; // 使用 "本週" 和 "上週"
|
||||
const electricityData = seriesNames.map((name) => {
|
||||
const dataPoints = weeks.map(() => {
|
||||
const value = faker.number.float({ min: 400, max: 800, precision: 0.1 }).toFixed(2); // 隨機用電量
|
||||
return { value };
|
||||
});
|
||||
|
||||
return {
|
||||
full_name: name, // 使用 "本週" 和 "上週"
|
||||
data: dataPoints,
|
||||
};
|
||||
});
|
||||
return electricityData;
|
||||
};
|
||||
|
||||
const fakeData = ref(generateFakeData()); // 儲存假資料
|
||||
|
||||
watch(
|
||||
() => fakeData.value, // 監控假資料
|
||||
(newValue) => {
|
||||
electricity_chart.value.chart.setOption({
|
||||
legend: {
|
||||
data: newValue.map(({ full_name }) => full_name),
|
||||
},
|
||||
xAxis: {
|
||||
data: weeks, // 直接使用星期幾陣列
|
||||
},
|
||||
series: newValue.map(({ full_name }, index) => ({
|
||||
name: full_name,
|
||||
type: "line",
|
||||
data: newValue[index].data.map(({ value }) => value),
|
||||
showSymbol: false,
|
||||
itemStyle: {
|
||||
color: SECOND_CHART_COLOR[index],
|
||||
},
|
||||
})),
|
||||
});
|
||||
},
|
||||
{
|
||||
deep: true,
|
||||
// 取得契約數據
|
||||
const getData = async () => {
|
||||
if (store.selectedBuilding.building_guid) {
|
||||
const res = await getDemand(store.selectedBuilding.building_guid);
|
||||
demandData.value = res.data[0];
|
||||
updateChart();
|
||||
}
|
||||
};
|
||||
|
||||
// 取得即時數據
|
||||
const getRealTime = async () => {
|
||||
if (store.selectedBuilding.building_guid) {
|
||||
const res = await getRealTimeDemand(store.selectedBuilding.building_guid);
|
||||
realTimeDemand.value = res.data.reverse();
|
||||
updateChart();
|
||||
}
|
||||
};
|
||||
|
||||
// 更新圖表資料
|
||||
const updateChart = () => {
|
||||
if (!demandData.value || !realTimeDemand.value.length) return;
|
||||
|
||||
defaultChartOption.value = {
|
||||
...defaultChartOption.value,
|
||||
legend: {
|
||||
...defaultChartOption.value.legend,
|
||||
data: [
|
||||
t("energy.real_time_Trend"),
|
||||
t("energy.contract_capacity"),
|
||||
t("energy.alert_capacity"),
|
||||
t("energy.reset_value"),
|
||||
],
|
||||
},
|
||||
xAxis: {
|
||||
...defaultChartOption.value.xAxis,
|
||||
data: realTimeDemand.value.map(({ time }) =>
|
||||
dayjs(time).format("HH:mm:ss")
|
||||
),
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: t("energy.real_time_Trend"),
|
||||
type: "line",
|
||||
data: realTimeDemand.value.map((d) => d.value),
|
||||
smooth: true,
|
||||
lineStyle: { width: 3 },
|
||||
itemStyle: { color: "#17CEE3" },
|
||||
},
|
||||
{
|
||||
name: t("energy.contract_capacity"),
|
||||
type: "line",
|
||||
data: Array(realTimeDemand.value.length).fill(
|
||||
demandData.value.contract
|
||||
),
|
||||
smooth: true,
|
||||
lineStyle: { width: 3 },
|
||||
itemStyle: { color: "#E4EA00" },
|
||||
},
|
||||
{
|
||||
name: t("energy.alert_capacity"),
|
||||
type: "line",
|
||||
data: Array(realTimeDemand.value.length).fill(demandData.value.alert),
|
||||
smooth: true,
|
||||
lineStyle: { width: 3 },
|
||||
itemStyle: { color: "#62E39A" },
|
||||
},
|
||||
{
|
||||
name: t("energy.reset_value"),
|
||||
type: "line",
|
||||
data: Array(realTimeDemand.value.length).fill(demandData.value.reset),
|
||||
smooth: true,
|
||||
lineStyle: { width: 3 },
|
||||
itemStyle: { color: "#E9971F" },
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
// 確保 `chart` 有更新
|
||||
if (demand_chart.value?.chart) {
|
||||
// demand_chart.value.chart.clear();
|
||||
demand_chart.value.chart.setOption(defaultChartOption.value);
|
||||
}
|
||||
};
|
||||
|
||||
// 監聽建築變更時重新抓取數據
|
||||
watch(
|
||||
() => store.selectedBuilding,
|
||||
(newBuilding) => {
|
||||
if (newBuilding) {
|
||||
getData();
|
||||
getRealTime();
|
||||
// 每 30 秒重新取得即時數據
|
||||
if (intervalId.value) {
|
||||
clearInterval(intervalId.value);
|
||||
}
|
||||
// 設置新的定時器
|
||||
intervalId.value = setInterval(getRealTime, 30000);
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
// 在元件掛載後,觸發一次 watch
|
||||
fakeData.value = generateFakeData(); // 初始化假資料
|
||||
onUnmounted(() => {
|
||||
// 清除定時器
|
||||
clearInterval(intervalId.value); // 使用 intervalId.value
|
||||
intervalId.value = null; // 清空 intervalId
|
||||
console.log("Interval cleared!");
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- 用電量 -->
|
||||
<h3 class="text-info font-bold text-xl text-center">用電量</h3>
|
||||
<div class="mb-3 relative">
|
||||
<h3 class="text-info text-xl text-center">
|
||||
{{ $t("energy.immediate_demand") }}
|
||||
{{
|
||||
realTimeDemand.length > 0
|
||||
? realTimeDemand[realTimeDemand.length - 1].value
|
||||
: "---"
|
||||
}}
|
||||
kw
|
||||
</h3>
|
||||
<DashboardElectricityModal :demandData="demandData" :getData="getData" />
|
||||
</div>
|
||||
<LineChart
|
||||
id="dashboard_electricity"
|
||||
id="immediate_demand_chart"
|
||||
class="min-h-[300px] max-h-fit"
|
||||
:option="defaultChartOption"
|
||||
ref="electricity_chart"
|
||||
ref="demand_chart"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
<style lang="scss" scoped></style>
|
||||
|
133
src/views/dashboard/components/DashboardElectricityModal.vue
Normal file
133
src/views/dashboard/components/DashboardElectricityModal.vue
Normal file
@ -0,0 +1,133 @@
|
||||
<script setup>
|
||||
import { inject, defineProps, watch, ref } from "vue";
|
||||
import useFormErrorMessage from "@/hooks/useFormErrorMessage";
|
||||
import { postEditDemand } from "@/apis/energy";
|
||||
import * as yup from "yup";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import useBuildingStore from "@/stores/useBuildingStore";
|
||||
const store = useBuildingStore();
|
||||
const { t } = useI18n();
|
||||
const { openToast } = inject("app_toast");
|
||||
const props = defineProps({
|
||||
demandData: Object,
|
||||
getData: Function,
|
||||
});
|
||||
|
||||
let scheme = yup.object({
|
||||
contract: yup.number().required(t("button.required")),
|
||||
alert: yup.number().required(t("button.required")),
|
||||
reset: yup.number().required(t("button.required")),
|
||||
});
|
||||
|
||||
const form = ref(null);
|
||||
const formState = ref({
|
||||
contract: null,
|
||||
alert: null,
|
||||
reset: null,
|
||||
});
|
||||
|
||||
const { formErrorMsg, handleSubmit, handleErrorReset } = useFormErrorMessage(
|
||||
scheme.value
|
||||
);
|
||||
|
||||
watch(
|
||||
() => props.demandData,
|
||||
(newValue) => {
|
||||
if (newValue) {
|
||||
formState.value = {
|
||||
...newValue,
|
||||
};
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const onOk = async () => {
|
||||
const values = await handleSubmit(scheme, formState.value);
|
||||
|
||||
const res = await postEditDemand({
|
||||
...values,
|
||||
"building_guid":store.selectedBuilding.building_guid,
|
||||
});
|
||||
if (res.isSuccess) {
|
||||
props.getData();
|
||||
closeModal();
|
||||
} else {
|
||||
openToast("error", res.msg, "#immediate_demand_add_item");
|
||||
}
|
||||
};
|
||||
|
||||
const closeModal = () => {
|
||||
handleErrorReset();
|
||||
onCancel();
|
||||
};
|
||||
|
||||
const openModal = () => {
|
||||
immediate_demand_add_item.showModal();
|
||||
};
|
||||
|
||||
const onCancel = () => {
|
||||
immediate_demand_add_item.close();
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<button
|
||||
class="btn btn-sm btn-success absolute top-0 right-0"
|
||||
@click.stop.prevent="openModal"
|
||||
>
|
||||
{{ $t("button.edit") }}
|
||||
</button>
|
||||
<Modal
|
||||
id="immediate_demand_add_item"
|
||||
:title="t('energy.edit_automatic_demand')"
|
||||
:onCancel="closeModal"
|
||||
width="400"
|
||||
>
|
||||
<template #modalContent>
|
||||
<form ref="form" class="mt-5 flex flex-col items-center">
|
||||
<Input :value="formState" class="w-full" name="contract">
|
||||
<template #topLeft>{{ $t("energy.contract_capacity") }}</template>
|
||||
<template #bottomLeft>
|
||||
<span class="text-error text-base">
|
||||
{{ formErrorMsg.contract }}
|
||||
</span>
|
||||
</template>
|
||||
</Input>
|
||||
<Input class="w-full" :value="formState" name="alert">
|
||||
<template #topLeft>{{ $t("energy.alert_capacity") }}</template>
|
||||
<template #bottomLeft>
|
||||
<span class="text-error text-base">
|
||||
{{ formErrorMsg.alert }}
|
||||
</span>
|
||||
</template>
|
||||
</Input>
|
||||
<Input class="w-full" :value="formState" name="reset">
|
||||
<template #topLeft>{{ $t("energy.reset_value") }}</template>
|
||||
<template #bottomLeft>
|
||||
<span class="text-error text-base">
|
||||
{{ formErrorMsg.reset }}
|
||||
</span>
|
||||
</template>
|
||||
</Input>
|
||||
</form>
|
||||
</template>
|
||||
<template #modalAction>
|
||||
<button
|
||||
type="reset"
|
||||
class="btn btn-outline-success mr-2"
|
||||
@click.prevent="closeModal"
|
||||
>
|
||||
{{ $t("button.cancel") }}
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
class="btn btn-outline-success"
|
||||
@click.prevent="onOk"
|
||||
>
|
||||
{{ $t("button.submit") }}
|
||||
</button>
|
||||
</template>
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped></style>
|
@ -1,117 +1,158 @@
|
||||
<script setup>
|
||||
import LineChart from "@/components/chart/LineChart.vue";
|
||||
import { SECOND_CHART_COLOR } from "@/constant";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { ref, computed, onMounted, defineProps, watch } from "vue";
|
||||
import dayjs from "dayjs";
|
||||
import { faker } from '@faker-js/faker'; // 引入 faker.js
|
||||
|
||||
const weeks = ["週日", "週一", "週二", "週三", "週四", "週五", "週六"];
|
||||
|
||||
const props = defineProps({
|
||||
data: Array,
|
||||
import BarChart from "@/components/chart/BarChart.vue";
|
||||
import { ref, onMounted, computed, watch } from "vue";
|
||||
import DashboardEmissionModal from "./DashboardEmissionModal.vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { getCarbonValue, getTaipower } from "@/apis/energy";
|
||||
import useBuildingStore from "@/stores/useBuildingStore";
|
||||
const store = useBuildingStore();
|
||||
const { t, locale } = useI18n();
|
||||
const taipower_data = ref([]);
|
||||
const carbonValue = ref(null);
|
||||
const carbonData = ref(null);
|
||||
const search_data = computed(() => {
|
||||
return {
|
||||
coefficient: carbonValue.value,
|
||||
building_guid: store.selectedBuilding?.building_guid || null,
|
||||
department_id_list: store.deptList.map((item) => item.key),
|
||||
floor_guid_list: store.floorList.map((item) => item.key),
|
||||
};
|
||||
});
|
||||
|
||||
const electricity_chart = ref(null);
|
||||
const defaultChartOption = {
|
||||
const defaultChartOption = ref({
|
||||
tooltip: {
|
||||
trigger: "axis",
|
||||
axisPointer: {
|
||||
type: "shadow",
|
||||
},
|
||||
},
|
||||
legend: {
|
||||
data: [],
|
||||
textStyle: {
|
||||
color: "#ffffff",
|
||||
fontSize: 16,
|
||||
fontSize: 14,
|
||||
},
|
||||
orient: "horizontal",
|
||||
top: "0%",
|
||||
},
|
||||
grid: {
|
||||
top: "10%",
|
||||
left: "0%",
|
||||
right: "0%",
|
||||
right: "2%",
|
||||
bottom: "0%",
|
||||
containLabel: true,
|
||||
},
|
||||
xAxis: {
|
||||
type: "category",
|
||||
splitLine: {
|
||||
show: false,
|
||||
},
|
||||
data: [],
|
||||
axisLabel: {
|
||||
color: "#ffffff",
|
||||
},
|
||||
data: weeks,
|
||||
},
|
||||
yAxis: {
|
||||
type: "value",
|
||||
splitLine: {
|
||||
show: false,
|
||||
},
|
||||
axisLabel: {
|
||||
color: "#ffffff",
|
||||
},
|
||||
},
|
||||
series: [],
|
||||
series: [
|
||||
{
|
||||
name: "",
|
||||
type: "bar",
|
||||
data: [],
|
||||
itemStyle: {
|
||||
color: "#17CEE3",
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const updateChartNames = () => {
|
||||
defaultChartOption.value.legend.data = [t("energy.carbon_equivalent")];
|
||||
defaultChartOption.value.series[0].name = t("energy.carbon_equivalent");
|
||||
};
|
||||
|
||||
const generateFakeData = () => {
|
||||
const seriesNames = ["設備 1", "設備 2", "設備 3", "設備 4"]; // 4 個設備
|
||||
const electricityData = seriesNames.map((name) => {
|
||||
const dataPoints = weeks.map(() => {
|
||||
const value = faker.number.float({ min: 400, max: 800, precision: 0.1 }).toFixed(2); // 隨機用電量
|
||||
return { value };
|
||||
});
|
||||
|
||||
return {
|
||||
full_name: name, // 使用設備名稱
|
||||
data: dataPoints,
|
||||
};
|
||||
});
|
||||
return electricityData;
|
||||
const getData = async (value) => {
|
||||
const res = await getTaipower(value);
|
||||
if (res.isSuccess) {
|
||||
taipower_data.value = res.data
|
||||
? res.data.sort((a, b) => a.month.localeCompare(b.month))
|
||||
: [];
|
||||
}
|
||||
};
|
||||
|
||||
const fakeData = ref(generateFakeData()); // 儲存假資料
|
||||
const getCarbonData = async () => {
|
||||
if (store.selectedBuilding.building_guid) {
|
||||
const res = await getCarbonValue(store.selectedBuilding.building_guid);
|
||||
carbonData.value = res.data[0];
|
||||
carbonValue.value = res.data[0]?.coefficient;
|
||||
}
|
||||
};
|
||||
|
||||
watch(
|
||||
() => fakeData.value, // 監控假資料
|
||||
(newValue) => {
|
||||
electricity_chart.value.chart.setOption({
|
||||
legend: {
|
||||
data: newValue.map(({ full_name }) => full_name),
|
||||
},
|
||||
xAxis: {
|
||||
data: weeks, // 直接使用星期幾陣列
|
||||
},
|
||||
series: newValue.map(({ full_name }, index) => ({
|
||||
name: full_name,
|
||||
type: "line",
|
||||
data: newValue[index].data.map(({ value }) => value),
|
||||
showSymbol: false,
|
||||
itemStyle: {
|
||||
color: SECOND_CHART_COLOR[index],
|
||||
},
|
||||
})),
|
||||
});
|
||||
() => store.selectedBuilding,
|
||||
(newBuilding) => {
|
||||
if (newBuilding) {
|
||||
getCarbonData();
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
watch(
|
||||
search_data,
|
||||
(newValue, oldValue) => {
|
||||
if (
|
||||
newValue.building_guid &&
|
||||
newValue.coefficient &&
|
||||
JSON.stringify(newValue) !== JSON.stringify(oldValue)
|
||||
) {
|
||||
getData(newValue);
|
||||
}
|
||||
},
|
||||
{
|
||||
immediate: true,
|
||||
deep: true,
|
||||
}
|
||||
);
|
||||
|
||||
// 監聽 taipower_data 的變化
|
||||
watch(
|
||||
taipower_data,
|
||||
() => {
|
||||
// 提取月份和碳排放數據
|
||||
const months = taipower_data.value.map((item) => item.month);
|
||||
const carbonTotal = taipower_data.value.map((item) => item.carbon);
|
||||
|
||||
// 更新圖表的 xAxis 和 series
|
||||
defaultChartOption.value.xAxis.data = months;
|
||||
defaultChartOption.value.series[0].data = carbonTotal;
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
|
||||
// 監聽 locale 變化
|
||||
watch(locale, () => {
|
||||
updateChartNames();
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
// 在元件掛載後,觸發一次 watch
|
||||
fakeData.value = generateFakeData(); // 初始化假資料
|
||||
updateChartNames();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- 碳排放趨勢 -->
|
||||
<h3 class="text-info font-bold text-xl text-center">碳排放趨勢</h3>
|
||||
<LineChart
|
||||
id="dashboard_electricity"
|
||||
<div class="mb-3 relative">
|
||||
<h3 class="font-bold text-xl text-center">
|
||||
<span class="text-info">
|
||||
{{ $t("energy.monthly_carbon_emission_and_reduction") }}
|
||||
</span>
|
||||
</h3>
|
||||
<DashboardEmissionModal :carbonData="carbonData" :getData="getCarbonData" />
|
||||
</div>
|
||||
<BarChart
|
||||
id="electricity_bill_chart"
|
||||
class="min-h-[300px] max-h-fit"
|
||||
:option="defaultChartOption"
|
||||
ref="electricity_chart"
|
||||
ref="bill_chart"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped></style>
|
110
src/views/dashboard/components/DashboardEmissionModal.vue
Normal file
110
src/views/dashboard/components/DashboardEmissionModal.vue
Normal file
@ -0,0 +1,110 @@
|
||||
<script setup>
|
||||
import { inject, defineProps, watch, ref } from "vue";
|
||||
import useFormErrorMessage from "@/hooks/useFormErrorMessage";
|
||||
import { postEditCarbonValue } from "@/apis/energy";
|
||||
import * as yup from "yup";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import useBuildingStore from "@/stores/useBuildingStore";
|
||||
const store = useBuildingStore();
|
||||
const { t } = useI18n();
|
||||
const { openToast } = inject("app_toast");
|
||||
const props = defineProps({
|
||||
carbonData: Object,
|
||||
getData: Function,
|
||||
});
|
||||
|
||||
let scheme = yup.object({
|
||||
coefficient: yup.number().required(t("button.required")),
|
||||
});
|
||||
|
||||
const form = ref(null);
|
||||
const formState = ref({
|
||||
coefficient: null,
|
||||
});
|
||||
|
||||
const { formErrorMsg, handleSubmit, handleErrorReset } = useFormErrorMessage(
|
||||
scheme.value
|
||||
);
|
||||
|
||||
watch(
|
||||
() => props.carbonData,
|
||||
(newValue) => {
|
||||
if (newValue) {
|
||||
formState.value = {
|
||||
...newValue,
|
||||
};
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const onOk = async () => {
|
||||
const values = await handleSubmit(scheme, formState.value);
|
||||
|
||||
const res = await postEditCarbonValue({
|
||||
...values,
|
||||
"building_guid":store.selectedBuilding.building_guid,
|
||||
});
|
||||
if (res.isSuccess) {
|
||||
props.getData();
|
||||
closeModal();
|
||||
} else {
|
||||
openToast("error", res.msg, "#carbon_emission_item");
|
||||
}
|
||||
};
|
||||
|
||||
const openModal = () => {
|
||||
carbon_emission_item.showModal();
|
||||
};
|
||||
|
||||
const onCancel = () => {
|
||||
carbon_emission_item.close();
|
||||
};
|
||||
|
||||
const closeModal = () => {
|
||||
handleErrorReset();
|
||||
onCancel();
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<button class="btn btn-sm btn-success absolute top-0 right-0" @click.stop.prevent="openModal">
|
||||
{{ $t("button.edit") }}
|
||||
</button>
|
||||
<Modal
|
||||
id="carbon_emission_item"
|
||||
:title="t('energy.edit_carbon_emission')"
|
||||
:onCancel="closeModal"
|
||||
width="400"
|
||||
>
|
||||
<template #modalContent>
|
||||
<form ref="form" class="mt-5 flex flex-col items-center">
|
||||
<Input :value="formState" class="w-full" name="coefficient">
|
||||
<template #topLeft>{{$t('energy.carbon_emission_coefficient')}}</template>
|
||||
<template #bottomLeft>
|
||||
<span class="text-error text-base">
|
||||
{{ formErrorMsg.coefficient }}
|
||||
</span>
|
||||
</template>
|
||||
</Input>
|
||||
</form>
|
||||
</template>
|
||||
<template #modalAction>
|
||||
<button
|
||||
type="reset"
|
||||
class="btn btn-outline-success mr-2"
|
||||
@click.prevent="closeModal"
|
||||
>
|
||||
{{ $t("button.cancel") }}
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
class="btn btn-outline-success"
|
||||
@click.prevent="onOk"
|
||||
>
|
||||
{{ $t("button.submit") }}
|
||||
</button>
|
||||
</template>
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped></style>
|
139
src/views/dashboard/components/DashboardIndoorTemp.vue
Normal file
139
src/views/dashboard/components/DashboardIndoorTemp.vue
Normal file
@ -0,0 +1,139 @@
|
||||
<script setup>
|
||||
import LineChart from "@/components/chart/LineChart.vue";
|
||||
import { SECOND_CHART_COLOR } from "@/constant";
|
||||
import dayjs from "dayjs";
|
||||
import { ref, inject, onMounted, onUnmounted, watch } from "vue";
|
||||
import { getDashboardTemp } from "@/apis/dashboard";
|
||||
import useSearchParams from "@/hooks/useSearchParam";
|
||||
import useBuildingStore from "@/stores/useBuildingStore";
|
||||
|
||||
const { searchParams } = useSearchParams();
|
||||
const buildingStore = useBuildingStore();
|
||||
const intervalType = "indoor";
|
||||
|
||||
const defaultChartOption = ref({
|
||||
tooltip: {
|
||||
trigger: "axis",
|
||||
},
|
||||
legend: {
|
||||
data: [],
|
||||
textStyle: {
|
||||
color: "#ffffff",
|
||||
fontSize: 16,
|
||||
},
|
||||
},
|
||||
grid: {
|
||||
top: "10%",
|
||||
left: "0%",
|
||||
right: "0%",
|
||||
bottom: "0%",
|
||||
containLabel: true,
|
||||
},
|
||||
xAxis: {
|
||||
// type: "time",
|
||||
type: "category",
|
||||
splitLine: {
|
||||
show: false,
|
||||
},
|
||||
axisLabel: {
|
||||
color: "#ffffff",
|
||||
},
|
||||
|
||||
data: [],
|
||||
},
|
||||
yAxis: {
|
||||
type: "value",
|
||||
splitLine: {
|
||||
show: false,
|
||||
},
|
||||
axisLabel: {
|
||||
color: "#ffffff",
|
||||
},
|
||||
// 移除固定的 min/max,讓 ECharts 自動計算範圍
|
||||
},
|
||||
series: [],
|
||||
});
|
||||
|
||||
const indoor_temp_chart = ref(null);
|
||||
|
||||
const data = ref([]);
|
||||
const getData = async (timeInterval) => {
|
||||
const res = await getDashboardTemp({
|
||||
building_guid: buildingStore.selectedBuilding.building_guid,
|
||||
timeInterval, // 時間間隔=>1.4.8
|
||||
tempOption: 1, // 1:室溫 2:冷藏
|
||||
});
|
||||
if (res.isSuccess) {
|
||||
console.log('室內溫度資料:', res.data['室溫']);
|
||||
|
||||
data.value = res.data['室溫'];
|
||||
}
|
||||
};
|
||||
|
||||
watch(
|
||||
data,
|
||||
(newValue) => {
|
||||
|
||||
newValue.length > 0 &&
|
||||
indoor_temp_chart.value.chart.setOption({
|
||||
legend: {
|
||||
data: newValue.map(({ full_name }) => full_name),
|
||||
},
|
||||
xAxis: {
|
||||
data: newValue[0]?.data.map(({ time }) => time), // 使用 time
|
||||
},
|
||||
series: newValue.map(({ full_name, data }, index) => ({
|
||||
name: full_name,
|
||||
type: "line",
|
||||
data: data.map(({ value }) => value),
|
||||
showSymbol: false,
|
||||
itemStyle: {
|
||||
color: SECOND_CHART_COLOR[index],
|
||||
},
|
||||
})),
|
||||
});
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
|
||||
// 監聽建築物選擇變化
|
||||
watch(
|
||||
() => buildingStore.selectedBuilding?.building_guid,
|
||||
(newBuildingGuid) => {
|
||||
if (newBuildingGuid) {
|
||||
getData(1);
|
||||
timeoutTimer.value = setInterval(() => {
|
||||
getData(1);
|
||||
}, 60 * 1000);
|
||||
} else {
|
||||
// 清除定時器
|
||||
if (timeoutTimer.value) {
|
||||
clearInterval(timeoutTimer.value);
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
const timeoutTimer = ref("");
|
||||
|
||||
onUnmounted(() => {
|
||||
// 清除定時器
|
||||
if (timeoutTimer.value) {
|
||||
clearInterval(timeoutTimer.value);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<h3 class="text-info font-bold text-xl text-center mb-3 relative">
|
||||
<span>室內溫度</span>
|
||||
</h3>
|
||||
|
||||
<LineChart
|
||||
id="dashboard_indoor_temp"
|
||||
class="min-h-[300px] max-h-fit"
|
||||
:option="defaultChartOption"
|
||||
ref="indoor_temp_chart"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped></style>
|
@ -2,16 +2,13 @@
|
||||
import LineChart from "@/components/chart/LineChart.vue";
|
||||
import { SECOND_CHART_COLOR } from "@/constant";
|
||||
import dayjs from "dayjs";
|
||||
import { ref, inject, onMounted, watch } from "vue";
|
||||
import { getDashboardTemp } from "@/apis/dashboard"; // 註解掉,因為我們產生假資料
|
||||
import { ref, inject, onMounted, onUnmounted, watch } from "vue";
|
||||
import { getDashboardTemp } from "@/apis/dashboard";
|
||||
import useSearchParams from "@/hooks/useSearchParam";
|
||||
import clearChart from "@/util/clearChart"; // 這些工具函式可以保留,但這裡暫時用不到
|
||||
import showChartLoading from "@/util/showChartLoading";
|
||||
import { faker } from "@faker-js/faker"; // 引入 faker.js
|
||||
import useBuildingStore from "@/stores/useBuildingStore";
|
||||
|
||||
const { searchParams } = useSearchParams();
|
||||
|
||||
const intervalType = "frozen";
|
||||
const buildingStore = useBuildingStore();
|
||||
|
||||
const defaultChartOption = ref({
|
||||
tooltip: {
|
||||
@ -51,8 +48,7 @@ const defaultChartOption = ref({
|
||||
axisLabel: {
|
||||
color: "#ffffff",
|
||||
},
|
||||
min: -20, // 設定 Y 軸最小值
|
||||
max: -15, // 設定 Y 軸最大值
|
||||
// 移除固定的 min/max,讓 ECharts 自動計算範圍
|
||||
},
|
||||
series: [],
|
||||
});
|
||||
@ -61,35 +57,21 @@ const frozen_temp_chart = ref(null);
|
||||
|
||||
const data = ref([]);
|
||||
const getData = async (timeInterval) => {
|
||||
// showChartLoading(frozen_temp_chart.value.chart); // 註解掉,因為我們產生假資料
|
||||
|
||||
// 假資料產生
|
||||
const numberOfSeries = 3; // 假設有 3 個冷凍庫
|
||||
const numberOfDataPoints = 24; // 假設有 24 個時間點 (每小時一個)
|
||||
const now = dayjs();
|
||||
const freezerData = Array.from({ length: numberOfSeries }, (_, i) => {
|
||||
const freezerName = `冷凍庫 ${i + 1}`;
|
||||
const dataPoints = Array.from({ length: numberOfDataPoints }, (_, j) => {
|
||||
const time = now
|
||||
.subtract(numberOfDataPoints - 1 - j, "hour")
|
||||
.format("HH:mm"); //過去24小時
|
||||
const value = faker.number.float({ min: -20, max: -18, precision: 0.1 }).toFixed(2); // 隨機溫度在 -25 到 -15 度之間
|
||||
return { time, value };
|
||||
});
|
||||
|
||||
return {
|
||||
full_name: freezerName,
|
||||
data: dataPoints,
|
||||
};
|
||||
const res = await getDashboardTemp({
|
||||
building_guid: buildingStore.selectedBuilding.building_guid,
|
||||
timeInterval, // 時間間隔=>1.4.8
|
||||
tempOption: 2, // 1:室溫 2:冷藏
|
||||
});
|
||||
|
||||
data.value = freezerData;
|
||||
if (res.isSuccess) {
|
||||
console.log('冷藏溫度資料:', res.data['冷藏溫度']);
|
||||
|
||||
data.value = res.data['冷藏溫度'];
|
||||
}
|
||||
};
|
||||
|
||||
watch(
|
||||
data,
|
||||
(newValue) => {
|
||||
// clearChart(frozen_temp_chart.value.chart); // 註解掉,因為我們產生假資料
|
||||
newValue.length > 0 &&
|
||||
frozen_temp_chart.value.chart.setOption({
|
||||
legend: {
|
||||
@ -108,22 +90,40 @@ watch(
|
||||
},
|
||||
})),
|
||||
});
|
||||
// frozen_temp_chart.value.chart.hideLoading(); // 註解掉,因為我們產生假資料
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
|
||||
// 監聽建築物選擇變化
|
||||
watch(
|
||||
() => buildingStore.selectedBuilding?.building_guid,
|
||||
(newBuildingGuid) => {
|
||||
if (newBuildingGuid) {
|
||||
getData(1);
|
||||
timeoutTimer.value = setInterval(() => {
|
||||
getData(1);
|
||||
}, 60 * 1000);
|
||||
} else {
|
||||
// 清除定時器
|
||||
if (timeoutTimer.value) {
|
||||
clearInterval(timeoutTimer.value);
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
const timeoutTimer = ref("");
|
||||
onMounted(() => {
|
||||
getData(1);
|
||||
timeoutTimer.value = setInterval(() => {
|
||||
getData(1);
|
||||
}, 60 * 60 * 1000);
|
||||
|
||||
onUnmounted(() => {
|
||||
// 清除定時器
|
||||
if (timeoutTimer.value) {
|
||||
clearInterval(timeoutTimer.value);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<h3 class="text-info font-bold text-xl text-center mb-3 relative">
|
||||
<span>溫度趨勢</span>
|
||||
<span>冷藏溫度</span>
|
||||
</h3>
|
||||
|
||||
<LineChart
|
@ -38,13 +38,13 @@ const currentData = computed(() => {
|
||||
class="w-5 h-5 rounded-full"
|
||||
:style="{
|
||||
backgroundColor:
|
||||
device[2]?.state === 'offline' ? 'red' : 'green',
|
||||
device[2]?.bgColor,
|
||||
}"
|
||||
></span>
|
||||
<span class="mx-2">{{ $t("system.status") }}:</span>
|
||||
<span>{{ device[2]?.state }}</span>
|
||||
</div>
|
||||
<button
|
||||
<!-- <button
|
||||
class="btn-text border-0"
|
||||
@click.prevent="
|
||||
(e) => {
|
||||
@ -61,7 +61,7 @@ const currentData = computed(() => {
|
||||
"
|
||||
>
|
||||
{{ $t("system.details") }}
|
||||
</button>
|
||||
</button> -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -134,8 +134,8 @@ const currentData = computed(() => {
|
||||
display: block;
|
||||
border-radius: 5px;
|
||||
margin-right: 10px;
|
||||
width: 2rem !important;
|
||||
height: 2rem;
|
||||
width: 1.5rem !important;
|
||||
height: 1.5rem;
|
||||
}
|
||||
|
||||
.item .sec02 span:nth-child(2) {
|
||||
|
@ -1,39 +1,75 @@
|
||||
<script setup>
|
||||
import { ref, onMounted, watch } from "vue";
|
||||
import { useRoute } from "vue-router";
|
||||
import ImmediateDemandChart from "./components/ImmediateDemandChart.vue";
|
||||
import CurrentInformation from "./components/CurrentInformation.vue";
|
||||
import UsageInformation from "./components/UsageInformation.vue";
|
||||
import ElectricityBillChart from "./components/ElectricityBillChart.vue";
|
||||
import BillingDegreeChart from "./components/BillingDegreeChart.vue";
|
||||
import { getRealTimeData, getElecUseMonth, getElecUseofDay } from "@/apis/energy";
|
||||
import { ref, onMounted, provide, onBeforeUnmount } from "vue";
|
||||
|
||||
import EnergyChart from "./components/EnergyChart/EnergyChart.vue";
|
||||
import EnergyHistoryTable from "./components/EnergyHistoryTable/EnergyHistoryTable.vue";
|
||||
import EnergyReport from "./components/EnergyReport/EnergyReport.vue";
|
||||
|
||||
const route = useRoute();
|
||||
const currentComponent = ref(null);
|
||||
|
||||
const updateComponent = () => {
|
||||
const { main_system_id, sub_system_id } = route.params;
|
||||
|
||||
if (main_system_id === "energy_chart") {
|
||||
if (sub_system_id === "chart") {
|
||||
currentComponent.value = EnergyChart;
|
||||
} else {
|
||||
currentComponent.value = EnergyHistoryTable;
|
||||
}
|
||||
} else if (main_system_id === "energy_report") {
|
||||
currentComponent.value = EnergyReport;
|
||||
} else {
|
||||
currentComponent.value = null;
|
||||
const realTime_data = ref([]);
|
||||
const interval = ref(null);
|
||||
const elecMonth_data = ref([]);
|
||||
const elecDay_data = ref([]);
|
||||
const getRealTime = async () => {
|
||||
const res = await getRealTimeData();
|
||||
if (res.isSuccess) {
|
||||
realTime_data.value = res.data[0];
|
||||
}
|
||||
};
|
||||
const getElecMonth = async () => {
|
||||
const res = await getElecUseMonth();
|
||||
if (res.isSuccess) {
|
||||
elecMonth_data.value = res.data.sort((a, b) => a.time.localeCompare(b.time));
|
||||
}
|
||||
};
|
||||
const getElecDay = async () => {
|
||||
const res = await getElecUseofDay();
|
||||
if (res.isSuccess) {
|
||||
elecDay_data.value = res.data;
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(updateComponent);
|
||||
const getRealtimeIntervalData = () => {
|
||||
interval.value = setInterval(() => {
|
||||
getRealTime();
|
||||
}, 1000 * 60);
|
||||
};
|
||||
|
||||
watch(
|
||||
() => route.params,
|
||||
() => {
|
||||
updateComponent();
|
||||
}
|
||||
);
|
||||
onMounted(() => {
|
||||
getRealTime();
|
||||
getRealtimeIntervalData();
|
||||
getElecMonth();
|
||||
getElecDay();
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
clearInterval(interval.value);
|
||||
});
|
||||
|
||||
provide("energy_data", { realTime_data, elecMonth_data, elecDay_data });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<component :is="currentComponent" />
|
||||
<div class="grid gap-4 grid-cols-5 h-[47%] mb-4">
|
||||
<div class="col-span-3">
|
||||
<ImmediateDemandChart />
|
||||
</div>
|
||||
<div class="col-span-2">
|
||||
<CurrentInformation />
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid gap-4 grid-cols-3 h-[47%]">
|
||||
<div class="col-span-1">
|
||||
<UsageInformation />
|
||||
</div>
|
||||
<div class="col-span-1">
|
||||
<ElectricityBillChart />
|
||||
</div>
|
||||
<div class="col-span-1">
|
||||
<BillingDegreeChart />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
|
110
src/views/energyManagement/components/BillingDegreeChart.vue
Normal file
110
src/views/energyManagement/components/BillingDegreeChart.vue
Normal file
@ -0,0 +1,110 @@
|
||||
<script setup>
|
||||
import BarChart from "@/components/chart/BarChart.vue";
|
||||
import { ref, watch, inject } from "vue";
|
||||
const { elecMonth_data } = inject("energy_data");
|
||||
|
||||
const defaultChartOption = ref({
|
||||
tooltip: {
|
||||
trigger: "axis",
|
||||
axisPointer: {
|
||||
type: "shadow",
|
||||
},
|
||||
},
|
||||
legend: {
|
||||
data: ["尖峰", "半尖峰", "離峰度數"],
|
||||
textStyle: {
|
||||
color: "#ffffff",
|
||||
fontSize: 16,
|
||||
},
|
||||
orient: "horizontal",
|
||||
bottom: "0%",
|
||||
},
|
||||
grid: {
|
||||
top: "5%",
|
||||
left: "0%",
|
||||
right: "0%",
|
||||
bottom: "10%",
|
||||
containLabel: true,
|
||||
},
|
||||
xAxis: {
|
||||
type: "category",
|
||||
data: [],
|
||||
axisLabel: {
|
||||
color: "#ffffff",
|
||||
},
|
||||
},
|
||||
yAxis: {
|
||||
type: "value",
|
||||
max:15,
|
||||
axisLabel: {
|
||||
color: "#ffffff",
|
||||
},
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: "尖峰",
|
||||
type: "bar",
|
||||
stack: "total",
|
||||
data: [],
|
||||
itemStyle: {
|
||||
color: "#3c50e0",
|
||||
},
|
||||
barWidth: '20px',
|
||||
},
|
||||
{
|
||||
name: "半尖峰",
|
||||
type: "bar",
|
||||
stack: "total",
|
||||
data: [],
|
||||
itemStyle: {
|
||||
color: "#6577f3",
|
||||
},
|
||||
barWidth: '20px',
|
||||
},
|
||||
{
|
||||
name: "離峰度數",
|
||||
type: "bar",
|
||||
stack: "total",
|
||||
data: [],
|
||||
itemStyle: {
|
||||
color: "#8fd0ef",
|
||||
},
|
||||
barWidth: '20px',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
watch(
|
||||
() => elecMonth_data.value,
|
||||
(newData) => {
|
||||
const times = newData.map((item) => item.time);
|
||||
const degreePeak = newData.map((item) => item.degreePeak);
|
||||
const degreeHalfPeak = newData.map((item) => item.degreeHalfPeak);
|
||||
const degreeOffPeak = newData.map((item) => item.degreeOffPeak);
|
||||
|
||||
defaultChartOption.value.xAxis.data = times;
|
||||
defaultChartOption.value.series[0].data = degreePeak;
|
||||
defaultChartOption.value.series[1].data = degreeHalfPeak;
|
||||
defaultChartOption.value.series[2].data = degreeOffPeak;
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="card bg-normal w-full h-full border border-cyan-300/50 rounded-md"
|
||||
>
|
||||
<div class="card-body">
|
||||
<h2 class="card-title">每月計費度數 (kWh)</h2>
|
||||
<BarChart
|
||||
id="billing_degree_chart"
|
||||
class="h-full w-full"
|
||||
:option="defaultChartOption"
|
||||
ref="degree_chart"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped></style>
|
91
src/views/energyManagement/components/CurrentInformation.vue
Normal file
91
src/views/energyManagement/components/CurrentInformation.vue
Normal file
@ -0,0 +1,91 @@
|
||||
<script setup>
|
||||
import { ref, watch, inject } from "vue";
|
||||
import dayjs from "dayjs";
|
||||
const { realTime_data } = inject("energy_data");
|
||||
|
||||
const voltage = ref([
|
||||
{ point: "線電壓 V12", value: 0 },
|
||||
{ point: "線電壓 V23", value: 0 },
|
||||
{ point: "線電壓 V31", value: 0 },
|
||||
]);
|
||||
const current = ref([
|
||||
{ point: "電流 1", value: 0 },
|
||||
{ point: "電流 2", value: 0 },
|
||||
{ point: "電流 3", value: 0 },
|
||||
]);
|
||||
const lastUpdated = ref("");
|
||||
|
||||
watch(
|
||||
() => realTime_data.value,
|
||||
(newData) => {
|
||||
newData.data.forEach(({ point, value }) => {
|
||||
switch (point) {
|
||||
case "U12":
|
||||
voltage.value[0].value = value;
|
||||
break;
|
||||
case "U23":
|
||||
voltage.value[1].value = value;
|
||||
break;
|
||||
case "U31":
|
||||
voltage.value[2].value = value;
|
||||
break;
|
||||
case "I1":
|
||||
current.value[0].value = value;
|
||||
break;
|
||||
case "I2":
|
||||
current.value[1].value = value;
|
||||
break;
|
||||
case "I3":
|
||||
current.value[2].value = value;
|
||||
break;
|
||||
}
|
||||
});
|
||||
lastUpdated.value = dayjs(newData.time).format("YYYY-MM-DD HH:mm:ss");
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="stats w-full h-[48%] p-2 mb-4 bg-slate-900 rounded-lg border border-cyan-200/20 shadow-md shadow-cyan-500/20"
|
||||
>
|
||||
<div
|
||||
v-for="(item, index) in voltage"
|
||||
:key="`voltage-${index}`"
|
||||
class="stat place-items-start"
|
||||
>
|
||||
<div class="stat-title text-gray-200 3xl:text-[2rem]">
|
||||
{{ item.point }} (V)
|
||||
</div>
|
||||
<div class="stat-value text-success text-5xl 3xl:text-[6rem]">
|
||||
{{ item.value }}
|
||||
</div>
|
||||
<div class="stat-desc text-gray-400 3xl:text-[2rem]">
|
||||
{{ lastUpdated }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="stats w-full h-[48%] p-2 mb-4 bg-slate-900 rounded-lg border border-cyan-200/20 shadow-md shadow-cyan-500/20"
|
||||
>
|
||||
<div
|
||||
v-for="(item, index) in current"
|
||||
:key="`current-${index}`"
|
||||
class="stat place-items-start"
|
||||
>
|
||||
<div class="stat-title text-gray-200 3xl:text-[2rem]">
|
||||
{{ item.point }} (A)
|
||||
</div>
|
||||
<div class="stat-value text-success text-5xl 3xl:text-[6rem]">
|
||||
{{ item.value }}
|
||||
</div>
|
||||
<div class="stat-desc text-gray-400 3xl:text-[2rem]">
|
||||
{{ lastUpdated }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped></style>
|
@ -0,0 +1,98 @@
|
||||
<script setup>
|
||||
import BarChart from "@/components/chart/BarChart.vue";
|
||||
import { ref, onMounted, inject, watch } from "vue";
|
||||
const { elecMonth_data } = inject("energy_data");
|
||||
const defaultChartOption = ref({
|
||||
tooltip: {
|
||||
trigger: "axis",
|
||||
axisPointer: {
|
||||
type: "shadow",
|
||||
},
|
||||
},
|
||||
legend: {
|
||||
data: ["基本電費", "流動電費"],
|
||||
textStyle: {
|
||||
color: "#ffffff",
|
||||
fontSize: 16,
|
||||
},
|
||||
orient: "horizontal",
|
||||
bottom: "0%",
|
||||
},
|
||||
grid: {
|
||||
top: "5%",
|
||||
left: "0%",
|
||||
right: "0%",
|
||||
bottom: "10%",
|
||||
containLabel: true,
|
||||
},
|
||||
xAxis: {
|
||||
type: "category",
|
||||
data: [],
|
||||
axisLabel: {
|
||||
color: "#ffffff",
|
||||
},
|
||||
},
|
||||
yAxis: {
|
||||
type: "value",
|
||||
max: 500,
|
||||
min: 0,
|
||||
axisLabel: {
|
||||
color: "#ffffff",
|
||||
},
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: "基本電費",
|
||||
type: "bar",
|
||||
stack: "total",
|
||||
data: [],
|
||||
itemStyle: {
|
||||
color: "#37c640",
|
||||
},
|
||||
barWidth: '20px',
|
||||
},
|
||||
{
|
||||
name: "流動電費",
|
||||
type: "bar",
|
||||
stack: "total",
|
||||
data: [],
|
||||
itemStyle: {
|
||||
color: "#8ee894",
|
||||
},
|
||||
barWidth: '20px',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
watch(
|
||||
() => elecMonth_data.value,
|
||||
(newData) => {
|
||||
const times = newData.map((item) => item.time);
|
||||
const costBase = newData.map((item) => item.costBase);
|
||||
const costDemand = newData.map(
|
||||
(item) => (item.costPeak + item.costHalfPeak + item.costOffPeak).toFixed(2)
|
||||
);
|
||||
|
||||
defaultChartOption.value.xAxis.data = times;
|
||||
defaultChartOption.value.series[0].data = costBase;
|
||||
defaultChartOption.value.series[1].data = costDemand;
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="card bg-normal w-full h-full border border-cyan-300/50 rounded-md"
|
||||
>
|
||||
<div class="card-body">
|
||||
<h2 class="card-title">每月電費分析</h2>
|
||||
<BarChart
|
||||
id="electricity_bill_chart"
|
||||
class="h-full w-full"
|
||||
:option="defaultChartOption"
|
||||
ref="bill_chart"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
121
src/views/energyManagement/components/ImmediateDemandChart.vue
Normal file
121
src/views/energyManagement/components/ImmediateDemandChart.vue
Normal file
@ -0,0 +1,121 @@
|
||||
<script setup>
|
||||
import LineChart from "@/components/chart/LineChart.vue";
|
||||
import { ref, onMounted, watch, inject } from "vue";
|
||||
const { elecDay_data } = inject("energy_data");
|
||||
const todayUsage = ref(0);
|
||||
const demand_chart = ref(null);
|
||||
const defaultChartOption = ref({
|
||||
tooltip: {
|
||||
trigger: "axis",
|
||||
},
|
||||
legend: {
|
||||
data: ["每日用電度數"],
|
||||
textStyle: {
|
||||
color: "#ffffff",
|
||||
fontSize: 16,
|
||||
},
|
||||
orient: "horizontal",
|
||||
bottom: "0%",
|
||||
},
|
||||
grid: {
|
||||
top: "10%",
|
||||
left: "0%",
|
||||
right: "0%",
|
||||
bottom: "15%",
|
||||
containLabel: true,
|
||||
},
|
||||
xAxis: {
|
||||
type: "category",
|
||||
splitLine: {
|
||||
show: false,
|
||||
},
|
||||
axisLabel: {
|
||||
color: "#ffffff",
|
||||
},
|
||||
data: [],
|
||||
},
|
||||
yAxis: {
|
||||
type: "value",
|
||||
splitLine: {
|
||||
show: false,
|
||||
},
|
||||
axisLabel: {
|
||||
color: "#ffffff",
|
||||
},
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: "每日用電度數",
|
||||
type: "line",
|
||||
data: [],
|
||||
smooth: true,
|
||||
lineStyle: {
|
||||
width: 3,
|
||||
},
|
||||
itemStyle: {
|
||||
color: "#34b7f1",
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
watch(
|
||||
() => elecDay_data.value,
|
||||
(newData) => {
|
||||
if (newData.length > 0) {
|
||||
const categories = [];
|
||||
const seriesData = [];
|
||||
|
||||
newData.forEach((item) => {
|
||||
categories.push(item.time.split(" ")[0]);
|
||||
const dailyUsage =(item.degreePeak + item.degreeHalfPeak + item.degreeOffPeak).toFixed(1);
|
||||
seriesData.push(dailyUsage);
|
||||
});
|
||||
demand_chart.value.chart.setOption({
|
||||
xAxis: {
|
||||
data: categories,
|
||||
},
|
||||
series:{
|
||||
data: seriesData,
|
||||
}
|
||||
});
|
||||
|
||||
todayUsage.value = seriesData[seriesData.length - 1];
|
||||
}
|
||||
},
|
||||
{
|
||||
deep: true,
|
||||
}
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="card bg-normal relative w-full h-full border border-cyan-300 rounded-sm"
|
||||
>
|
||||
<div class="card-body">
|
||||
<h2 class="card-title">
|
||||
最新用電度數
|
||||
<p>{{ todayUsage }} kWh</p>
|
||||
</h2>
|
||||
<LineChart
|
||||
id="immediate_demand_chart"
|
||||
class="h-full w-full"
|
||||
:option="defaultChartOption"
|
||||
ref="demand_chart"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.bg-normal::after {
|
||||
@apply absolute bottom-1 right-1 h-5 w-5 bg-no-repeat z-10 bg-[url('@/assets/img/table/content-box-background05.svg')] bg-center;
|
||||
content: "";
|
||||
}
|
||||
|
||||
.bg-normal::before {
|
||||
@apply absolute -top-3 -right-[10px] h-8 w-8 bg-no-repeat z-10 bg-[url('@/assets/img/table/content-box-background02.svg')] bg-center;
|
||||
content: "";
|
||||
}
|
||||
</style>
|
119
src/views/energyManagement/components/UsageInformation.vue
Normal file
119
src/views/energyManagement/components/UsageInformation.vue
Normal file
@ -0,0 +1,119 @@
|
||||
<script setup>
|
||||
import { ref, onMounted, inject, watch } from "vue";
|
||||
const { elecMonth_data } = inject("energy_data");
|
||||
|
||||
const totalElecBills = ref(0);
|
||||
const totalElecDegree = ref(0);
|
||||
const IntervalElecBills = ref(0);
|
||||
const IntervalElecDegree = ref(0);
|
||||
const totalDate = ref(null);
|
||||
const IntervalDate = ref(null);
|
||||
|
||||
const daysInMonth = (month) => {
|
||||
const [year, monthNumber] = month.split("-");
|
||||
return new Date(year, monthNumber, 0).getDate(); // 返回該月份的天數
|
||||
};
|
||||
|
||||
watch(
|
||||
() => elecMonth_data.value,
|
||||
(newData) => {
|
||||
// 計算總電費與用電度數
|
||||
totalElecBills.value = newData
|
||||
.reduce((sum, item) => {
|
||||
const monthlyBill =
|
||||
item.costPeak + item.costHalfPeak + item.costOffPeak + item.costBase;
|
||||
return sum + monthlyBill;
|
||||
}, 0)
|
||||
.toFixed(2);
|
||||
totalElecDegree.value = newData
|
||||
.reduce((sum, item) => {
|
||||
const monthlyBill =
|
||||
item.degreePeak + item.degreeHalfPeak + item.degreeOffPeak;
|
||||
return sum + monthlyBill;
|
||||
}, 0)
|
||||
.toFixed(2);
|
||||
|
||||
|
||||
// 計算區間電費與用電度數,取最新一筆數據
|
||||
const latestData = newData[newData.length - 1];
|
||||
IntervalElecBills.value = (
|
||||
latestData.costPeak +
|
||||
latestData.costHalfPeak +
|
||||
latestData.costOffPeak +
|
||||
latestData.costBase
|
||||
).toFixed(2);
|
||||
IntervalElecDegree.value = (
|
||||
latestData.degreePeak +
|
||||
latestData.degreeHalfPeak +
|
||||
latestData.degreeOffPeak
|
||||
).toFixed(2);
|
||||
const monthDays = latestData.time ? daysInMonth(latestData.time) : 0;
|
||||
IntervalDate.value = latestData.time
|
||||
? `${latestData.time}-01~${latestData.time}-${monthDays}`
|
||||
: "";
|
||||
|
||||
totalDate.value = `${newData[0].time}-01 ~ ${latestData.time}-${monthDays}`;
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="stats w-full h-[48%] p-2 mb-4 bg-slate-900 rounded-lg border border-cyan-200/20 shadow-md shadow-cyan-500/20"
|
||||
>
|
||||
<!-- 電壓資訊 -->
|
||||
<div class="stat place-items-start">
|
||||
<div class="stat-title text-gray-200 3xl:text-[2rem]">
|
||||
今年電費總計 (元)
|
||||
</div>
|
||||
<div class="stat-value text-success text-5xl 3xl:text-[6rem]">
|
||||
{{ totalElecBills }}
|
||||
</div>
|
||||
<div class="stat-desc text-gray-400 3xl:text-[2rem]">
|
||||
{{ totalDate }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat place-items-start">
|
||||
<div class="stat-title text-gray-200 3xl:text-[2rem]">
|
||||
區間電費總計 (元)
|
||||
</div>
|
||||
<div class="stat-value text-success text-5xl 3xl:text-[6rem]">
|
||||
{{ IntervalElecBills }}
|
||||
</div>
|
||||
<div class="stat-desc text-gray-400 3xl:text-[2rem]">
|
||||
{{ IntervalDate }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="stats w-full h-[48%] p-2 mb-4 bg-slate-900 rounded-lg border border-cyan-200/20 shadow-md shadow-cyan-500/20"
|
||||
>
|
||||
<!-- 電流資訊 -->
|
||||
<div class="stat place-items-start">
|
||||
<div class="stat-title text-gray-200 3xl:text-[2rem]">
|
||||
今年用電度數 (kWh)
|
||||
</div>
|
||||
<div class="stat-value text-success text-5xl 3xl:text-[6rem]">
|
||||
{{ totalElecDegree }}
|
||||
</div>
|
||||
<div class="stat-desc text-gray-400 3xl:text-[2rem]">
|
||||
{{ totalDate }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat place-items-start">
|
||||
<div class="stat-title text-gray-200 3xl:text-[2rem]">
|
||||
區間用電度數 (kWh)
|
||||
</div>
|
||||
<div class="stat-value text-success text-5xl 3xl:text-[6rem]">
|
||||
{{ IntervalElecDegree }}
|
||||
</div>
|
||||
<div class="stat-desc text-gray-400 3xl:text-[2rem]">
|
||||
{{ IntervalDate }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped></style>
|
Loading…
Reference in New Issue
Block a user