首頁串接資料與能源管理去掉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 ({
|
export const getDashboardTemp = async ({
|
||||||
timeInterval,
|
timeInterval,
|
||||||
tempOption,
|
tempOption,
|
||||||
typeOption = "",
|
building_guid,
|
||||||
}) => {
|
}) => {
|
||||||
const res = typeOption
|
const res = await instance.post(GET_DASHBOARD_TEMP_API, {
|
||||||
? await instance.post(GET_DASHBOARD_TEMP_API, {
|
timeInterval,
|
||||||
timeInterval,
|
tempOption,
|
||||||
tempOption,
|
building_guid,
|
||||||
typeOption,
|
});
|
||||||
})
|
|
||||||
: await instance.post(GET_DASHBOARD_TEMP_API, {
|
|
||||||
timeInterval,
|
|
||||||
tempOption,
|
|
||||||
});
|
|
||||||
console.log(res);
|
|
||||||
return apihandler(res.code, res.data, {
|
return apihandler(res.code, res.data, {
|
||||||
msg: res.msg,
|
msg: res.msg,
|
||||||
code: res.code,
|
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_REALTIME_DIST_API = `/api/Energe/GetRealTimeDistribution`;
|
||||||
export const GET_ELECUSE_DAY_API = `/api/Energe/GetElecUseDay`;
|
export const GET_ELECUSE_DAY_API = `/api/Energe/GetElecUseDay`;
|
||||||
export const GET_TAI_POWER_API = `/api/Energe/GetTaipower`;
|
export const GET_TAI_POWER_API = `/api/Energe/GetTaipower`;
|
||||||
|
@ -1,4 +1,7 @@
|
|||||||
import {
|
import {
|
||||||
|
GET_REALTIME_DATA_API,
|
||||||
|
GET_ELEC_MONTH_API,
|
||||||
|
GET_ELEC_DAY_API,
|
||||||
GET_REALTIME_DIST_API,
|
GET_REALTIME_DIST_API,
|
||||||
GET_ELECUSE_DAY_API,
|
GET_ELECUSE_DAY_API,
|
||||||
GET_TAI_POWER_API,
|
GET_TAI_POWER_API,
|
||||||
@ -18,6 +21,33 @@ import instance, { fileInstance } from "@/util/request";
|
|||||||
import apihandler from "@/util/apihandler";
|
import apihandler from "@/util/apihandler";
|
||||||
import downloadExcel from "@/util/downloadExcel";
|
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 ({
|
export const getRealTimeDist = async ({
|
||||||
building_guid,
|
building_guid,
|
||||||
department_id_list,
|
department_id_list,
|
||||||
|
@ -18,15 +18,9 @@ export const getSystemFloors = async (building_tag, sub_system_tag) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getSystemDevices = async ({
|
export const getSystemDevices = async ({ building_guid }) => {
|
||||||
sub_system_tag,
|
|
||||||
building_guid,
|
|
||||||
department_id_list,
|
|
||||||
}) => {
|
|
||||||
const res = await instance.post(GET_SYSTEM_DEVICE_LIST_API, {
|
const res = await instance.post(GET_SYSTEM_DEVICE_LIST_API, {
|
||||||
sub_system_tag,
|
|
||||||
building_guid,
|
building_guid,
|
||||||
department_id_list,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return apihandler(res.code, res.data, {
|
return apihandler(res.code, res.data, {
|
||||||
|
@ -30,8 +30,8 @@ const open = ref(false);
|
|||||||
const showDrawer = async (authCode) => {
|
const showDrawer = async (authCode) => {
|
||||||
if (authCode === "PF1") {
|
if (authCode === "PF1") {
|
||||||
emit("show-drawer", buildingStore.selectedBuilding.building_guid); // 使用 emit 呼叫父組件的函数
|
emit("show-drawer", buildingStore.selectedBuilding.building_guid); // 使用 emit 呼叫父組件的函数
|
||||||
} else if (authCode === "PF2" || authCode === "PF11") {
|
} else if (authCode === "PF11") {
|
||||||
emit("getSubPage", authCode === "PF2" ? "Energy" : "Setting");
|
emit("getSubPage", "Setting");
|
||||||
}
|
}
|
||||||
currentAuthCode.value = authCode;
|
currentAuthCode.value = authCode;
|
||||||
open.value = true;
|
open.value = true;
|
||||||
@ -75,11 +75,7 @@ const handleOpenChange = (keys) => {
|
|||||||
:key="page.authCode"
|
:key="page.authCode"
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
v-if="
|
v-if="page.authCode === 'PF1' || page.authCode === 'PF11'"
|
||||||
page.authCode === 'PF1' ||
|
|
||||||
page.authCode === 'PF2' ||
|
|
||||||
page.authCode === 'PF11'
|
|
||||||
"
|
|
||||||
@click="showDrawer(page.authCode)"
|
@click="showDrawer(page.authCode)"
|
||||||
:class="
|
:class="
|
||||||
twMerge(
|
twMerge(
|
||||||
@ -87,10 +83,6 @@ const handleOpenChange = (keys) => {
|
|||||||
page.authCode === 'PF1' && route.fullPath.includes('/system')
|
page.authCode === 'PF1' && route.fullPath.includes('/system')
|
||||||
? 'router-link-active router-link-exact-active'
|
? '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')
|
page.authCode === 'PF11' && route.fullPath.includes('/setting')
|
||||||
? 'router-link-active router-link-exact-active'
|
? 'router-link-active router-link-exact-active'
|
||||||
: ''
|
: ''
|
||||||
@ -144,24 +136,22 @@ const handleOpenChange = (keys) => {
|
|||||||
<a-menu-item
|
<a-menu-item
|
||||||
v-for="sub in currentAuthCode === 'PF1'
|
v-for="sub in currentAuthCode === 'PF1'
|
||||||
? main.history_Sub_systems
|
? main.history_Sub_systems
|
||||||
: currentAuthCode === 'PF2'
|
|
||||||
? main.sub
|
|
||||||
: main.sub"
|
: main.sub"
|
||||||
:key="sub.sub_system_tag + `_` + sub.type"
|
:key="sub.sub_system_tag + `_` + sub.type"
|
||||||
@click="() => {onClose(); closeDrawer();}"
|
@click="
|
||||||
|
() => {
|
||||||
|
onClose();
|
||||||
|
closeDrawer();
|
||||||
|
}
|
||||||
|
"
|
||||||
>
|
>
|
||||||
<router-link
|
<router-link
|
||||||
:to="{
|
:to="{
|
||||||
name:
|
name: currentAuthCode === 'PF11' ? 'setting' : 'sub_system',
|
||||||
currentAuthCode === 'PF2'
|
|
||||||
? 'energyManagement'
|
|
||||||
: currentAuthCode === 'PF11'
|
|
||||||
? 'setting'
|
|
||||||
: 'sub_system',
|
|
||||||
params: {
|
params: {
|
||||||
main_system_id: main.main_system_tag,
|
main_system_id: main.main_system_tag,
|
||||||
sub_system_id: sub.sub_system_tag,
|
sub_system_id: sub.sub_system_tag,
|
||||||
...(currentAuthCode === 'PF2' || currentAuthCode === 'PF11'
|
...(currentAuthCode === 'PF11'
|
||||||
? { type: sub.type }
|
? { type: sub.type }
|
||||||
: { floor_id: 'main' }),
|
: { floor_id: 'main' }),
|
||||||
},
|
},
|
||||||
@ -185,7 +175,7 @@ const handleOpenChange = (keys) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.menu-box .btn-group span {
|
.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 {
|
.menu-box > li {
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { defineStore } from "pinia";
|
import { defineStore } from "pinia";
|
||||||
import { ref, computed, watch } from "vue";
|
import { ref, computed, watch } from "vue";
|
||||||
import { useRoute } from "vue-router";
|
import { useRoute } from "vue-router";
|
||||||
import { getBuildings } from "@/apis/building";
|
import { getBuildings, getAllSysSidebar } from "@/apis/building";
|
||||||
import { getAssetFloorList, getDepartmentList } from "@/apis/asset";
|
import { getAssetFloorList, getDepartmentList } from "@/apis/asset";
|
||||||
|
|
||||||
const useBuildingStore = defineStore("buildingInfo", () => {
|
const useBuildingStore = defineStore("buildingInfo", () => {
|
||||||
@ -56,27 +56,39 @@ const useBuildingStore = defineStore("buildingInfo", () => {
|
|||||||
// 獲取樓層資料
|
// 獲取樓層資料
|
||||||
const fetchFloorList = async (building_guid) => {
|
const fetchFloorList = async (building_guid) => {
|
||||||
const res = await getAssetFloorList(building_guid);
|
const res = await getAssetFloorList(building_guid);
|
||||||
floorList.value = res.data[0]?.floors.map((d) => ({
|
floorList.value =
|
||||||
...d,
|
res.data[0]?.floors.map((d) => ({
|
||||||
title: d.full_name,
|
...d,
|
||||||
key: d.floor_guid,
|
title: d.full_name,
|
||||||
})) || [];
|
key: d.floor_guid,
|
||||||
|
})) || [];
|
||||||
};
|
};
|
||||||
|
|
||||||
// 獲取部門資料
|
// 獲取部門資料
|
||||||
const fetchDepartmentList = async () => {
|
const fetchDepartmentList = async () => {
|
||||||
const res = await getDepartmentList();
|
const res = await getDepartmentList();
|
||||||
deptList.value = res.data.map((d) => ({
|
deptList.value =
|
||||||
...d,
|
res.data.map((d) => ({
|
||||||
title: d.name,
|
...d,
|
||||||
key: d.id,
|
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) => {
|
watch(selectedBuilding, async (newBuilding) => {
|
||||||
if (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 DashboardFloorBar from "./components/DashboardFloorBar.vue";
|
||||||
import DashboardEffectScatter from "./components/DashboardEffectScatter.vue";
|
import DashboardEffectScatter from "./components/DashboardEffectScatter.vue";
|
||||||
import DashboardSysCard from "./components/DashboardSysCard.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 DashboardElectricity from "./components/DashboardElectricity.vue";
|
||||||
import DashboardEmission from "./components/DashboardEmission.vue";
|
import DashboardEmission from "./components/DashboardEmission.vue";
|
||||||
import DashboardAlert from "./components/DashboardAlert.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 FILE_BASEURL = import.meta.env.VITE_FILE_API_BASEURL;
|
||||||
|
const buildingStore = useBuildingStore()
|
||||||
|
|
||||||
const systemData = {
|
const subscribeData = ref([]);
|
||||||
'85e7d4b3-9570-4149-95d4-3739c00cee6a': [
|
const systemData = ref({});
|
||||||
[
|
const getData = async () => {
|
||||||
120.1,
|
const res = await getSystemDevices({
|
||||||
480,
|
building_guid: buildingStore.selectedBuilding?.building_guid,
|
||||||
{
|
})
|
||||||
full_name: "空氣偵測器",
|
|
||||||
state: "online",
|
subscribeData.value = res.data
|
||||||
temperature: "25°C",
|
console.log("devices", subscribeData.value)
|
||||||
icon: `${FILE_BASEURL}/upload/device_icon/3454f5e0-3afa-4ace-ae54-a68bf7183e7d.png`,
|
|
||||||
bgColor: "rgba(100, 166, 182,0.5)",
|
// 轉換資料格式
|
||||||
bgSize: 60,
|
const transformedData = {};
|
||||||
},
|
|
||||||
],
|
subscribeData.value.forEach(floor => {
|
||||||
[
|
if (floor.device_list && floor.device_list.length > 0) {
|
||||||
580,
|
const fullUrl = floor.floor_map_name;
|
||||||
800,
|
const uuid = fullUrl ? fullUrl.replace(/\.svg$/, "") : "";
|
||||||
{
|
transformedData[uuid] = floor.device_list.map(device => {
|
||||||
full_name: "空調",
|
// 解析座標
|
||||||
state: "offline",
|
const coordinates = JSON.parse(device.device_coordinate || '[0,0]');
|
||||||
temperature: "-5°C",
|
const x = coordinates[0];
|
||||||
icon: `${FILE_BASEURL}/upload/device_icon/9d8e1dd3-8187-46e3-8a6a-ae116210ecff.png`,
|
const y = coordinates[1];
|
||||||
bgColor: "rgba(255, 0, 0,0.5)",
|
|
||||||
bgSize: 60,
|
// 決定設備狀態和顏色
|
||||||
},
|
let state = "online";
|
||||||
],
|
let bgColor = "rgba(255, 255, 255)";
|
||||||
[
|
|
||||||
940,
|
if (device.device_status === "offline" || device.device_status === null) {
|
||||||
960,
|
state = "offline";
|
||||||
{
|
bgColor = "rgba(34, 51, 85)";
|
||||||
full_name: "電錶",
|
}
|
||||||
state: "online",
|
|
||||||
temperature: "25°C",
|
return [
|
||||||
icon: `${FILE_BASEURL}/upload/device_icon/83deea51-a97e-4757-ba15-5cd94cb25929.png`,
|
x,
|
||||||
bgColor: "rgba(100, 166, 182,0.5)",
|
y,
|
||||||
bgSize: 60,
|
{
|
||||||
},
|
full_name: device.full_name,
|
||||||
],
|
state: state,
|
||||||
],
|
icon: device.device_image ? `${FILE_BASEURL}/upload/device_icon/${device.device_image}` : '',
|
||||||
'5cf3c8da-a5b4-42da-8b1a-14d5a44a0456': [
|
bgColor: bgColor,
|
||||||
[
|
bgSize: 50,
|
||||||
280,
|
}
|
||||||
280,
|
];
|
||||||
{
|
});
|
||||||
full_name: "空氣偵測器",
|
}
|
||||||
state: "offline",
|
});
|
||||||
temperature: "25°C",
|
|
||||||
icon: `${FILE_BASEURL}/upload/device_icon/3454f5e0-3afa-4ace-ae54-a68bf7183e7d.png`,
|
console.log("transformedData", transformedData);
|
||||||
bgColor: "rgba(255, 0, 0,0.5)",
|
systemData.value = transformedData;
|
||||||
bgSize: 60,
|
}
|
||||||
},
|
|
||||||
],
|
watch(
|
||||||
[
|
() => buildingStore.selectedBuilding,
|
||||||
2800,
|
(newBuilding) => {
|
||||||
3000,
|
if (newBuilding) {
|
||||||
{
|
getData();
|
||||||
full_name: "電錶",
|
}
|
||||||
state: "online",
|
},
|
||||||
temperature: "25°C",
|
{
|
||||||
icon: `${FILE_BASEURL}/upload/device_icon/83deea51-a97e-4757-ba15-5cd94cb25929.png`,
|
immediate: true
|
||||||
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,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
],
|
|
||||||
};
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<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"
|
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>
|
<div>
|
||||||
<DashboardTemp />
|
<DashboardRefrigTemp />
|
||||||
|
</div>
|
||||||
|
<div class="mt-10">
|
||||||
|
<DashboardIndoorTemp />
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-10">
|
<div class="mt-10">
|
||||||
<DashboardAlert />
|
<DashboardAlert />
|
||||||
|
@ -12,7 +12,7 @@ const FILE_BASEURL = import.meta.env.VITE_FILE_API_BASEURL;
|
|||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
data: {
|
data: {
|
||||||
type: Object,
|
type: Object,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const asset_floor_chart = ref(null);
|
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-lg">${params.data[2].full_name}</div>
|
||||||
<div class="text-sm text-gray-500">狀態: ${
|
<div class="text-sm text-gray-500">狀態: ${
|
||||||
params.data[2].state || ""
|
params.data[2].state || ""
|
||||||
}</div>
|
}</div>`
|
||||||
<div class="text-sm text-gray-500">溫度: ${
|
|
||||||
params.data[2].temperature || ""
|
|
||||||
}</div></div>`
|
|
||||||
: "";
|
: "";
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
map,
|
map,
|
||||||
roam: true,
|
roam: true, // 允許縮放和平移
|
||||||
|
layoutSize: window.innerWidth <= 768 ? "110%" : "75%",
|
||||||
|
layoutCenter: ["50%", "50%"],
|
||||||
scaleLimit: { min: 1, max: 2 },
|
scaleLimit: { min: 1, max: 2 },
|
||||||
},
|
},
|
||||||
series: [
|
series: [
|
||||||
@ -49,7 +48,7 @@ const defaultOption = (map, data = []) => {
|
|||||||
type: "scatter",
|
type: "scatter",
|
||||||
coordinateSystem: "geo",
|
coordinateSystem: "geo",
|
||||||
geoIndex: 0,
|
geoIndex: 0,
|
||||||
symbolSize: (value) => value[2]?.bgSize || 40,
|
symbolSize: (value) => value[2]?.bgSize || 50,
|
||||||
symbol: "range",
|
symbol: "range",
|
||||||
itemStyle: {
|
itemStyle: {
|
||||||
color: (params) => params.data[2]?.bgColor || "rgba(0,0,0,0.8)",
|
color: (params) => params.data[2]?.bgColor || "rgba(0,0,0,0.8)",
|
||||||
@ -65,11 +64,14 @@ const defaultOption = (map, data = []) => {
|
|||||||
tooltip: 2,
|
tooltip: 2,
|
||||||
},
|
},
|
||||||
symbolSize: 30,
|
symbolSize: 30,
|
||||||
|
itemStyle: {
|
||||||
|
color: "rgba(225,225,225,0.8)",
|
||||||
|
},
|
||||||
symbol: (value, params) => {
|
symbol: (value, params) => {
|
||||||
// 若有 icon 屬性就用 image,否則用圓點
|
// 若有 icon 屬性就用 image,否則用圓點
|
||||||
return params.data[2]?.icon
|
return params.data[2]?.icon
|
||||||
? `image://${params.data[2].icon}`
|
? `image://${params.data[2].icon}`
|
||||||
: "circle";
|
: "roundRect";
|
||||||
},
|
},
|
||||||
data,
|
data,
|
||||||
label: {
|
label: {
|
||||||
@ -78,7 +80,7 @@ const defaultOption = (map, data = []) => {
|
|||||||
formatter: (params) => params.data[2]?.full_name || "",
|
formatter: (params) => params.data[2]?.full_name || "",
|
||||||
color: "#333",
|
color: "#333",
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
backgroundColor: "rgba(255,255,255,0.7)",
|
backgroundColor: "rgba(255,255,255)",
|
||||||
padding: [4, 4],
|
padding: [4, 4],
|
||||||
borderRadius: 3,
|
borderRadius: 3,
|
||||||
},
|
},
|
||||||
@ -89,19 +91,19 @@ const defaultOption = (map, data = []) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const currentIconData = computed(() => {
|
const currentIconData = computed(() => {
|
||||||
return props.data[searchParams.value.floor_id] || [];
|
const data = props.data?.[searchParams.value.floor_id] || [];
|
||||||
|
return data;
|
||||||
});
|
});
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
[searchParams, () => asset_floor_chart],
|
[searchParams, () => asset_floor_chart.value, () => props.data],
|
||||||
([newValue, newChart], [oldValue]) => {
|
([newValue, newChart, newData], [oldValue]) => {
|
||||||
if (newValue.floor_id && newChart.value) {
|
if (newValue.floor_id && newChart && Object.keys(newData || {}).length > 0) {
|
||||||
asset_floor_chart.value.updateSvg(
|
newChart.updateSvg(
|
||||||
{
|
{
|
||||||
full_name: newValue.floor_id,
|
full_name: newValue.floor_id,
|
||||||
path: `${FILE_BASEURL}/upload/floor_map/${newValue.floor_id}.svg`,
|
path: `${FILE_BASEURL}/upload/floor_map/${newValue.floor_id}.svg`,
|
||||||
},
|
},
|
||||||
|
|
||||||
defaultOption(newValue.floor_id, currentIconData.value)
|
defaultOption(newValue.floor_id, currentIconData.value)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,31 +1,29 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import LineChart from "@/components/chart/LineChart.vue";
|
import LineChart from "@/components/chart/LineChart.vue";
|
||||||
import { SECOND_CHART_COLOR } from "@/constant";
|
import { ref, onMounted, watch, onUnmounted } from "vue";
|
||||||
import { twMerge } from "tailwind-merge";
|
import DashboardElectricityModal from "./DashboardElectricityModal.vue";
|
||||||
import { ref, computed, onMounted, defineProps, watch } from "vue";
|
import { getDemand, getRealTimeDemand } from "@/apis/energy";
|
||||||
|
import { useI18n } from "vue-i18n";
|
||||||
|
import useBuildingStore from "@/stores/useBuildingStore";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import { faker } from '@faker-js/faker'; // 引入 faker.js
|
|
||||||
|
|
||||||
const weeks = ["週日", "週一", "週二", "週三", "週四", "週五", "週六"];
|
const store = useBuildingStore();
|
||||||
|
const { t } = useI18n();
|
||||||
|
|
||||||
const props = defineProps({
|
const demandData = ref(null);
|
||||||
data: Array,
|
const realTimeDemand = ref([]);
|
||||||
});
|
const demand_chart = ref(null);
|
||||||
|
const intervalId = ref(null);
|
||||||
const electricity_chart = ref(null);
|
// 預設為空,避免 `null` 造成 `ECharts` 失敗
|
||||||
const defaultChartOption = {
|
const defaultChartOption = ref({
|
||||||
tooltip: {
|
tooltip: { trigger: "axis" },
|
||||||
trigger: "axis",
|
|
||||||
},
|
|
||||||
legend: {
|
legend: {
|
||||||
data: [],
|
data: [],
|
||||||
textStyle: {
|
textStyle: { color: "#ffffff", fontSize: 16 },
|
||||||
color: "#ffffff",
|
top: "0%",
|
||||||
fontSize: 16,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
grid: {
|
grid: {
|
||||||
top: "10%",
|
top: "20%",
|
||||||
left: "0%",
|
left: "0%",
|
||||||
right: "0%",
|
right: "0%",
|
||||||
bottom: "0%",
|
bottom: "0%",
|
||||||
@ -33,85 +31,147 @@ const defaultChartOption = {
|
|||||||
},
|
},
|
||||||
xAxis: {
|
xAxis: {
|
||||||
type: "category",
|
type: "category",
|
||||||
splitLine: {
|
splitLine: { show: false },
|
||||||
show: false,
|
axisLabel: { color: "#ffffff" },
|
||||||
},
|
data: [], // 預設空值,避免 undefined
|
||||||
axisLabel: {
|
|
||||||
color: "#ffffff",
|
|
||||||
},
|
|
||||||
data: weeks,
|
|
||||||
},
|
},
|
||||||
yAxis: {
|
yAxis: {
|
||||||
type: "value",
|
type: "value",
|
||||||
splitLine: {
|
splitLine: { show: false },
|
||||||
show: false,
|
axisLabel: { color: "#ffffff" },
|
||||||
},
|
|
||||||
axisLabel: {
|
|
||||||
color: "#ffffff",
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
series: [],
|
series: [],
|
||||||
};
|
});
|
||||||
|
|
||||||
const generateFakeData = () => {
|
// 取得契約數據
|
||||||
const seriesNames = ["本週", "上週"]; // 使用 "本週" 和 "上週"
|
const getData = async () => {
|
||||||
const electricityData = seriesNames.map((name) => {
|
if (store.selectedBuilding.building_guid) {
|
||||||
const dataPoints = weeks.map(() => {
|
const res = await getDemand(store.selectedBuilding.building_guid);
|
||||||
const value = faker.number.float({ min: 400, max: 800, precision: 0.1 }).toFixed(2); // 隨機用電量
|
demandData.value = res.data[0];
|
||||||
return { value };
|
updateChart();
|
||||||
});
|
|
||||||
|
|
||||||
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 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(() => {
|
onUnmounted(() => {
|
||||||
// 在元件掛載後,觸發一次 watch
|
// 清除定時器
|
||||||
fakeData.value = generateFakeData(); // 初始化假資料
|
clearInterval(intervalId.value); // 使用 intervalId.value
|
||||||
|
intervalId.value = null; // 清空 intervalId
|
||||||
|
console.log("Interval cleared!");
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<!-- 用電量 -->
|
<div class="mb-3 relative">
|
||||||
<h3 class="text-info font-bold text-xl text-center">用電量</h3>
|
<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
|
<LineChart
|
||||||
id="dashboard_electricity"
|
id="immediate_demand_chart"
|
||||||
class="min-h-[300px] max-h-fit"
|
class="min-h-[300px] max-h-fit"
|
||||||
:option="defaultChartOption"
|
:option="defaultChartOption"
|
||||||
ref="electricity_chart"
|
ref="demand_chart"
|
||||||
/>
|
/>
|
||||||
</template>
|
</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>
|
<script setup>
|
||||||
import LineChart from "@/components/chart/LineChart.vue";
|
import BarChart from "@/components/chart/BarChart.vue";
|
||||||
import { SECOND_CHART_COLOR } from "@/constant";
|
import { ref, onMounted, computed, watch } from "vue";
|
||||||
import { twMerge } from "tailwind-merge";
|
import DashboardEmissionModal from "./DashboardEmissionModal.vue";
|
||||||
import { ref, computed, onMounted, defineProps, watch } from "vue";
|
import { useI18n } from "vue-i18n";
|
||||||
import dayjs from "dayjs";
|
import { getCarbonValue, getTaipower } from "@/apis/energy";
|
||||||
import { faker } from '@faker-js/faker'; // 引入 faker.js
|
import useBuildingStore from "@/stores/useBuildingStore";
|
||||||
|
const store = useBuildingStore();
|
||||||
const weeks = ["週日", "週一", "週二", "週三", "週四", "週五", "週六"];
|
const { t, locale } = useI18n();
|
||||||
|
const taipower_data = ref([]);
|
||||||
const props = defineProps({
|
const carbonValue = ref(null);
|
||||||
data: Array,
|
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 defaultChartOption = ref({
|
||||||
const electricity_chart = ref(null);
|
|
||||||
const defaultChartOption = {
|
|
||||||
tooltip: {
|
tooltip: {
|
||||||
trigger: "axis",
|
trigger: "axis",
|
||||||
|
axisPointer: {
|
||||||
|
type: "shadow",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
legend: {
|
legend: {
|
||||||
data: [],
|
data: [],
|
||||||
textStyle: {
|
textStyle: {
|
||||||
color: "#ffffff",
|
color: "#ffffff",
|
||||||
fontSize: 16,
|
fontSize: 14,
|
||||||
},
|
},
|
||||||
|
orient: "horizontal",
|
||||||
|
top: "0%",
|
||||||
},
|
},
|
||||||
grid: {
|
grid: {
|
||||||
top: "10%",
|
top: "10%",
|
||||||
left: "0%",
|
left: "0%",
|
||||||
right: "0%",
|
right: "2%",
|
||||||
bottom: "0%",
|
bottom: "0%",
|
||||||
containLabel: true,
|
containLabel: true,
|
||||||
},
|
},
|
||||||
xAxis: {
|
xAxis: {
|
||||||
type: "category",
|
type: "category",
|
||||||
splitLine: {
|
data: [],
|
||||||
show: false,
|
|
||||||
},
|
|
||||||
axisLabel: {
|
axisLabel: {
|
||||||
color: "#ffffff",
|
color: "#ffffff",
|
||||||
},
|
},
|
||||||
data: weeks,
|
|
||||||
},
|
},
|
||||||
yAxis: {
|
yAxis: {
|
||||||
type: "value",
|
type: "value",
|
||||||
splitLine: {
|
|
||||||
show: false,
|
|
||||||
},
|
|
||||||
axisLabel: {
|
axisLabel: {
|
||||||
color: "#ffffff",
|
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 getData = async (value) => {
|
||||||
const seriesNames = ["設備 1", "設備 2", "設備 3", "設備 4"]; // 4 個設備
|
const res = await getTaipower(value);
|
||||||
const electricityData = seriesNames.map((name) => {
|
if (res.isSuccess) {
|
||||||
const dataPoints = weeks.map(() => {
|
taipower_data.value = res.data
|
||||||
const value = faker.number.float({ min: 400, max: 800, precision: 0.1 }).toFixed(2); // 隨機用電量
|
? res.data.sort((a, b) => a.month.localeCompare(b.month))
|
||||||
return { value };
|
: [];
|
||||||
});
|
}
|
||||||
|
|
||||||
return {
|
|
||||||
full_name: name, // 使用設備名稱
|
|
||||||
data: dataPoints,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
return electricityData;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
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(
|
watch(
|
||||||
() => fakeData.value, // 監控假資料
|
() => store.selectedBuilding,
|
||||||
(newValue) => {
|
(newBuilding) => {
|
||||||
electricity_chart.value.chart.setOption({
|
if (newBuilding) {
|
||||||
legend: {
|
getCarbonData();
|
||||||
data: newValue.map(({ full_name }) => full_name),
|
}
|
||||||
},
|
},
|
||||||
xAxis: {
|
{ immediate: true }
|
||||||
data: weeks, // 直接使用星期幾陣列
|
);
|
||||||
},
|
|
||||||
series: newValue.map(({ full_name }, index) => ({
|
watch(
|
||||||
name: full_name,
|
search_data,
|
||||||
type: "line",
|
(newValue, oldValue) => {
|
||||||
data: newValue[index].data.map(({ value }) => value),
|
if (
|
||||||
showSymbol: false,
|
newValue.building_guid &&
|
||||||
itemStyle: {
|
newValue.coefficient &&
|
||||||
color: SECOND_CHART_COLOR[index],
|
JSON.stringify(newValue) !== JSON.stringify(oldValue)
|
||||||
},
|
) {
|
||||||
})),
|
getData(newValue);
|
||||||
});
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
immediate: true,
|
||||||
deep: 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(() => {
|
onMounted(() => {
|
||||||
// 在元件掛載後,觸發一次 watch
|
updateChartNames();
|
||||||
fakeData.value = generateFakeData(); // 初始化假資料
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<!-- 碳排放趨勢 -->
|
<div class="mb-3 relative">
|
||||||
<h3 class="text-info font-bold text-xl text-center">碳排放趨勢</h3>
|
<h3 class="font-bold text-xl text-center">
|
||||||
<LineChart
|
<span class="text-info">
|
||||||
id="dashboard_electricity"
|
{{ $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"
|
class="min-h-[300px] max-h-fit"
|
||||||
:option="defaultChartOption"
|
:option="defaultChartOption"
|
||||||
ref="electricity_chart"
|
ref="bill_chart"
|
||||||
/>
|
/>
|
||||||
</template>
|
</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 LineChart from "@/components/chart/LineChart.vue";
|
||||||
import { SECOND_CHART_COLOR } from "@/constant";
|
import { SECOND_CHART_COLOR } from "@/constant";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import { ref, inject, onMounted, watch } from "vue";
|
import { ref, inject, onMounted, onUnmounted, watch } from "vue";
|
||||||
import { getDashboardTemp } from "@/apis/dashboard"; // 註解掉,因為我們產生假資料
|
import { getDashboardTemp } from "@/apis/dashboard";
|
||||||
import useSearchParams from "@/hooks/useSearchParam";
|
import useSearchParams from "@/hooks/useSearchParam";
|
||||||
import clearChart from "@/util/clearChart"; // 這些工具函式可以保留,但這裡暫時用不到
|
import useBuildingStore from "@/stores/useBuildingStore";
|
||||||
import showChartLoading from "@/util/showChartLoading";
|
|
||||||
import { faker } from "@faker-js/faker"; // 引入 faker.js
|
|
||||||
|
|
||||||
const { searchParams } = useSearchParams();
|
const { searchParams } = useSearchParams();
|
||||||
|
const buildingStore = useBuildingStore();
|
||||||
const intervalType = "frozen";
|
|
||||||
|
|
||||||
const defaultChartOption = ref({
|
const defaultChartOption = ref({
|
||||||
tooltip: {
|
tooltip: {
|
||||||
@ -51,8 +48,7 @@ const defaultChartOption = ref({
|
|||||||
axisLabel: {
|
axisLabel: {
|
||||||
color: "#ffffff",
|
color: "#ffffff",
|
||||||
},
|
},
|
||||||
min: -20, // 設定 Y 軸最小值
|
// 移除固定的 min/max,讓 ECharts 自動計算範圍
|
||||||
max: -15, // 設定 Y 軸最大值
|
|
||||||
},
|
},
|
||||||
series: [],
|
series: [],
|
||||||
});
|
});
|
||||||
@ -61,35 +57,21 @@ const frozen_temp_chart = ref(null);
|
|||||||
|
|
||||||
const data = ref([]);
|
const data = ref([]);
|
||||||
const getData = async (timeInterval) => {
|
const getData = async (timeInterval) => {
|
||||||
// showChartLoading(frozen_temp_chart.value.chart); // 註解掉,因為我們產生假資料
|
const res = await getDashboardTemp({
|
||||||
|
building_guid: buildingStore.selectedBuilding.building_guid,
|
||||||
// 假資料產生
|
timeInterval, // 時間間隔=>1.4.8
|
||||||
const numberOfSeries = 3; // 假設有 3 個冷凍庫
|
tempOption: 2, // 1:室溫 2:冷藏
|
||||||
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,
|
|
||||||
};
|
|
||||||
});
|
});
|
||||||
|
if (res.isSuccess) {
|
||||||
data.value = freezerData;
|
console.log('冷藏溫度資料:', res.data['冷藏溫度']);
|
||||||
|
|
||||||
|
data.value = res.data['冷藏溫度'];
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
data,
|
data,
|
||||||
(newValue) => {
|
(newValue) => {
|
||||||
// clearChart(frozen_temp_chart.value.chart); // 註解掉,因為我們產生假資料
|
|
||||||
newValue.length > 0 &&
|
newValue.length > 0 &&
|
||||||
frozen_temp_chart.value.chart.setOption({
|
frozen_temp_chart.value.chart.setOption({
|
||||||
legend: {
|
legend: {
|
||||||
@ -108,22 +90,40 @@ watch(
|
|||||||
},
|
},
|
||||||
})),
|
})),
|
||||||
});
|
});
|
||||||
// frozen_temp_chart.value.chart.hideLoading(); // 註解掉,因為我們產生假資料
|
|
||||||
},
|
},
|
||||||
{ deep: true }
|
{ 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("");
|
const timeoutTimer = ref("");
|
||||||
onMounted(() => {
|
|
||||||
getData(1);
|
onUnmounted(() => {
|
||||||
timeoutTimer.value = setInterval(() => {
|
// 清除定時器
|
||||||
getData(1);
|
if (timeoutTimer.value) {
|
||||||
}, 60 * 60 * 1000);
|
clearInterval(timeoutTimer.value);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<h3 class="text-info font-bold text-xl text-center mb-3 relative">
|
<h3 class="text-info font-bold text-xl text-center mb-3 relative">
|
||||||
<span>溫度趨勢</span>
|
<span>冷藏溫度</span>
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
<LineChart
|
<LineChart
|
@ -38,13 +38,13 @@ const currentData = computed(() => {
|
|||||||
class="w-5 h-5 rounded-full"
|
class="w-5 h-5 rounded-full"
|
||||||
:style="{
|
:style="{
|
||||||
backgroundColor:
|
backgroundColor:
|
||||||
device[2]?.state === 'offline' ? 'red' : 'green',
|
device[2]?.bgColor,
|
||||||
}"
|
}"
|
||||||
></span>
|
></span>
|
||||||
<span class="mx-2">{{ $t("system.status") }}:</span>
|
<span class="mx-2">{{ $t("system.status") }}:</span>
|
||||||
<span>{{ device[2]?.state }}</span>
|
<span>{{ device[2]?.state }}</span>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<!-- <button
|
||||||
class="btn-text border-0"
|
class="btn-text border-0"
|
||||||
@click.prevent="
|
@click.prevent="
|
||||||
(e) => {
|
(e) => {
|
||||||
@ -61,7 +61,7 @@ const currentData = computed(() => {
|
|||||||
"
|
"
|
||||||
>
|
>
|
||||||
{{ $t("system.details") }}
|
{{ $t("system.details") }}
|
||||||
</button>
|
</button> -->
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -134,8 +134,8 @@ const currentData = computed(() => {
|
|||||||
display: block;
|
display: block;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
margin-right: 10px;
|
margin-right: 10px;
|
||||||
width: 2rem !important;
|
width: 1.5rem !important;
|
||||||
height: 2rem;
|
height: 1.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.item .sec02 span:nth-child(2) {
|
.item .sec02 span:nth-child(2) {
|
||||||
|
@ -1,39 +1,75 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted, watch } from "vue";
|
import ImmediateDemandChart from "./components/ImmediateDemandChart.vue";
|
||||||
import { useRoute } from "vue-router";
|
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";
|
const realTime_data = ref([]);
|
||||||
import EnergyHistoryTable from "./components/EnergyHistoryTable/EnergyHistoryTable.vue";
|
const interval = ref(null);
|
||||||
import EnergyReport from "./components/EnergyReport/EnergyReport.vue";
|
const elecMonth_data = ref([]);
|
||||||
|
const elecDay_data = ref([]);
|
||||||
const route = useRoute();
|
const getRealTime = async () => {
|
||||||
const currentComponent = ref(null);
|
const res = await getRealTimeData();
|
||||||
|
if (res.isSuccess) {
|
||||||
const updateComponent = () => {
|
realTime_data.value = res.data[0];
|
||||||
const { main_system_id, sub_system_id } = route.params;
|
}
|
||||||
|
};
|
||||||
if (main_system_id === "energy_chart") {
|
const getElecMonth = async () => {
|
||||||
if (sub_system_id === "chart") {
|
const res = await getElecUseMonth();
|
||||||
currentComponent.value = EnergyChart;
|
if (res.isSuccess) {
|
||||||
} else {
|
elecMonth_data.value = res.data.sort((a, b) => a.time.localeCompare(b.time));
|
||||||
currentComponent.value = EnergyHistoryTable;
|
}
|
||||||
}
|
};
|
||||||
} else if (main_system_id === "energy_report") {
|
const getElecDay = async () => {
|
||||||
currentComponent.value = EnergyReport;
|
const res = await getElecUseofDay();
|
||||||
} else {
|
if (res.isSuccess) {
|
||||||
currentComponent.value = null;
|
elecDay_data.value = res.data;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
onMounted(updateComponent);
|
const getRealtimeIntervalData = () => {
|
||||||
|
interval.value = setInterval(() => {
|
||||||
|
getRealTime();
|
||||||
|
}, 1000 * 60);
|
||||||
|
};
|
||||||
|
|
||||||
watch(
|
onMounted(() => {
|
||||||
() => route.params,
|
getRealTime();
|
||||||
() => {
|
getRealtimeIntervalData();
|
||||||
updateComponent();
|
getElecMonth();
|
||||||
}
|
getElecDay();
|
||||||
);
|
});
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
clearInterval(interval.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
provide("energy_data", { realTime_data, elecMonth_data, elecDay_data });
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<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>
|
</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