首頁串接資料與能源管理去掉sidebar menu

This commit is contained in:
koko 2025-07-17 14:16:57 +08:00
parent 143a7ae061
commit bc51a9db1f
21 changed files with 1449 additions and 386 deletions

View File

@ -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, {
const res = await instance.post(GET_DASHBOARD_TEMP_API, {
timeInterval,
tempOption,
building_guid,
});
console.log(res);
return apihandler(res.code, res.data, {
msg: res.msg,
code: res.code,

View File

@ -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`;

View File

@ -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,

View File

@ -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, {

View File

@ -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' }),
},

View File

@ -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,7 +56,8 @@ const useBuildingStore = defineStore("buildingInfo", () => {
// 獲取樓層資料
const fetchFloorList = async (building_guid) => {
const res = await getAssetFloorList(building_guid);
floorList.value = res.data[0]?.floors.map((d) => ({
floorList.value =
res.data[0]?.floors.map((d) => ({
...d,
title: d.full_name,
key: d.floor_guid,
@ -66,17 +67,28 @@ const useBuildingStore = defineStore("buildingInfo", () => {
// 獲取部門資料
const fetchDepartmentList = async () => {
const res = await getDepartmentList();
deptList.value = res.data.map((d) => ({
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)
]);
}
});

View File

@ -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,
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: "空氣偵測器",
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,
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();
}
},
],
[
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,
},
],
],
};
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 />

View File

@ -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)
);
}

View File

@ -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,84 +31,146 @@ 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 getData = async () => {
if (store.selectedBuilding.building_guid) {
const res = await getDemand(store.selectedBuilding.building_guid);
demandData.value = res.data[0];
updateChart();
}
};
const fakeData = ref(generateFakeData()); //
//
const getRealTime = async () => {
if (store.selectedBuilding.building_guid) {
const res = await getRealTimeDemand(store.selectedBuilding.building_guid);
realTimeDemand.value = res.data.reverse();
updateChart();
}
};
watch(
() => fakeData.value, //
(newValue) => {
electricity_chart.value.chart.setOption({
//
const updateChart = () => {
if (!demandData.value || !realTimeDemand.value.length) return;
defaultChartOption.value = {
...defaultChartOption.value,
legend: {
data: newValue.map(({ full_name }) => full_name),
...defaultChartOption.value.legend,
data: [
t("energy.real_time_Trend"),
t("energy.contract_capacity"),
t("energy.alert_capacity"),
t("energy.reset_value"),
],
},
xAxis: {
data: weeks, // 使
...defaultChartOption.value.xAxis,
data: realTimeDemand.value.map(({ time }) =>
dayjs(time).format("HH:mm:ss")
),
},
series: newValue.map(({ full_name }, index) => ({
name: full_name,
series: [
{
name: t("energy.real_time_Trend"),
type: "line",
data: newValue[index].data.map(({ value }) => value),
showSymbol: false,
itemStyle: {
color: SECOND_CHART_COLOR[index],
},
})),
});
data: realTimeDemand.value.map((d) => d.value),
smooth: true,
lineStyle: { width: 3 },
itemStyle: { color: "#17CEE3" },
},
{
deep: true,
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>

View 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>

View File

@ -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: [],
};
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 };
series: [
{
name: "",
type: "bar",
data: [],
itemStyle: {
color: "#17CEE3",
},
},
],
});
return {
full_name: name, // 使
data: dataPoints,
};
});
return electricityData;
const updateChartNames = () => {
defaultChartOption.value.legend.data = [t("energy.carbon_equivalent")];
defaultChartOption.value.series[0].name = t("energy.carbon_equivalent");
};
const fakeData = ref(generateFakeData()); //
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 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),
() => store.selectedBuilding,
(newBuilding) => {
if (newBuilding) {
getCarbonData();
}
},
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],
},
})),
});
{ 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>

View 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>

View 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>

View File

@ -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 };
const res = await getDashboardTemp({
building_guid: buildingStore.selectedBuilding.building_guid,
timeInterval, // =>1.4.8
tempOption: 2, // 1 2:
});
if (res.isSuccess) {
console.log('冷藏溫度資料:', res.data['冷藏溫度']);
return {
full_name: freezerName,
data: dataPoints,
};
});
data.value = freezerData;
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 }
);
const timeoutTimer = ref("");
onMounted(() => {
//
watch(
() => buildingStore.selectedBuilding?.building_guid,
(newBuildingGuid) => {
if (newBuildingGuid) {
getData(1);
timeoutTimer.value = setInterval(() => {
getData(1);
}, 60 * 60 * 1000);
}, 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>
<span>冷藏溫度</span>
</h3>
<LineChart

View File

@ -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) {

View File

@ -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;
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];
}
} else if (main_system_id === "energy_report") {
currentComponent.value = EnergyReport;
} else {
currentComponent.value = null;
};
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>

View 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>

View 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>

View File

@ -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>

View 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>

View 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>