預設繁體中文 | 拿掉右上告警icon | 新創賦能 : 告警初始化後全選+search | 歷史資料加最大最小值chart、table

This commit is contained in:
koko 2025-07-24 14:32:48 +08:00
parent c48072a41c
commit 6e61679c1f
33 changed files with 1233 additions and 119 deletions

View File

@ -1,3 +1,5 @@
export const GET_SYSTEM_FLOOR_LIST_API = `/api/Device/GetFloor`; export const GET_SYSTEM_FLOOR_LIST_API = `/api/Device/GetFloor`;
export const GET_SYSTEM_DEVICE_LIST_API = `/api/Device/GetDeviceList`; export const GET_SYSTEM_DEVICE_LIST_API = `/api/Device/GetDeviceList`;
export const GET_SYSTEM_REALTIME_API = `/api/Device/GetRealTimeData`; export const GET_SYSTEM_REALTIME_API = `/api/Device/GetRealTimeData`;
export const GET_SYSTEM_CONFIG_API = `/api/GetSystemConfig`;

View File

@ -2,6 +2,7 @@ import {
GET_SYSTEM_FLOOR_LIST_API, GET_SYSTEM_FLOOR_LIST_API,
GET_SYSTEM_DEVICE_LIST_API, GET_SYSTEM_DEVICE_LIST_API,
GET_SYSTEM_REALTIME_API, GET_SYSTEM_REALTIME_API,
GET_SYSTEM_CONFIG_API
} from "./api"; } from "./api";
import instance from "@/util/request"; import instance from "@/util/request";
import apihandler from "@/util/apihandler"; import apihandler from "@/util/apihandler";
@ -36,3 +37,11 @@ export const getSystemRealTime = async (device_list) => {
code: res.code, code: res.code,
}); });
}; };
export const getSystemConfig = async (building_guid) => {
const res = await instance.post(GET_SYSTEM_CONFIG_API, { building_guid });
return apihandler(res.code, res.data, {
msg: res.msg,
code: res.code,
});
};

View File

@ -121,9 +121,9 @@ watch(locale, () => {
<li> <li>
<NavbarLang /> <NavbarLang />
</li> </li>
<li> <!-- <li>
<AlarmDrawer /> <AlarmDrawer />
</li> </li> -->
<li> <li>
<NavbarUser /> <NavbarUser />
</li> </li>

View File

@ -6,7 +6,7 @@ const store = useBuildingStore();
const selectBuilding = (bui) => { const selectBuilding = (bui) => {
store.selectedBuilding = bui; // selectedBuildingwatch store.selectedBuilding = bui; // selectedBuildingwatch
localStorage.setItem("CviBuilding", JSON.stringify(bui)); localStorage.setItem("EmpowerBuilding", JSON.stringify(bui));
}; };
onMounted(() => { onMounted(() => {

View File

@ -6,7 +6,7 @@ const { locale } = useI18n(); // 使用 I18n
// //
const toggleLanguage = (lang) => { const toggleLanguage = (lang) => {
locale.value = lang; locale.value = lang;
localStorage.setItem("CviLanguage", lang); localStorage.setItem("EmpowerLanguage", lang);
}; };
</script> </script>

View File

@ -1,9 +1,16 @@
<script setup> <script setup>
import useUserInfoStore from "@/stores/useUserInfoStore"; import useUserInfoStore from "@/stores/useUserInfoStore";
import { ref, onMounted } from "vue"; import { ref, onMounted } from "vue";
import { useRouter } from 'vue-router';
const router = useRouter();
const store = useUserInfoStore(); const store = useUserInfoStore();
const user = ref(""); const user = ref("");
function logout() {
router.push('/logout');
}
onMounted(() => { onMounted(() => {
const name = store.user.user_name; const name = store.user.user_name;
if (name) { if (name) {
@ -30,12 +37,12 @@ onMounted(() => {
class="dropdown-content translate-y-2 z-[100] menu py-3 shadow rounded w-32 bg-[#4c625e] border text-center" class="dropdown-content translate-y-2 z-[100] menu py-3 shadow rounded w-32 bg-[#4c625e] border text-center"
> >
<li class="text-white"> <li class="text-white">
<router-link <a
to="logout" type="button"
type="link"
class="flex flex-col justify-center items-center" class="flex flex-col justify-center items-center"
@click="logout"
>{{ $t("sign_out") }} >{{ $t("sign_out") }}
</router-link> </a>
</li> </li>
</ul> </ul>
</div> </div>

View File

@ -30,12 +30,12 @@ const messages = {
us, us,
}; };
const storedLanguage = localStorage.getItem("CviLanguage") || "us"; const storedLanguage = localStorage.getItem("EmpowerLanguage") || "tw";
const i18n = createI18n({ const i18n = createI18n({
legacy: false, legacy: false,
locale: storedLanguage, locale: storedLanguage,
fallbackLocale: 'us', fallbackLocale: 'tw',
messages, messages,
}); });
const app = createApp(App); const app = createApp(App);

View File

@ -106,6 +106,7 @@ router.beforeEach(async (to, from, next) => {
document.cookie = "user_name=; Max-Age=0"; document.cookie = "user_name=; Max-Age=0";
auth.user.token = ""; auth.user.token = "";
auth.user.user_name = ""; auth.user.user_name = "";
localStorage.removeItem("EmpowerBuilding");
window.location.reload(); window.location.reload();
next({ path: "/login" }); next({ path: "/login" });
} }

View File

@ -3,6 +3,7 @@ import { ref, computed, watch } from "vue";
import { useRoute } from "vue-router"; import { useRoute } from "vue-router";
import { getBuildings, getAllSysSidebar } from "@/apis/building"; import { getBuildings, getAllSysSidebar } from "@/apis/building";
import { getAssetFloorList, getDepartmentList } from "@/apis/asset"; import { getAssetFloorList, getDepartmentList } from "@/apis/asset";
import { getSystemConfig } from "@/apis/system";
const useBuildingStore = defineStore("buildingInfo", () => { const useBuildingStore = defineStore("buildingInfo", () => {
// 狀態定義 // 狀態定義
@ -11,6 +12,7 @@ const useBuildingStore = defineStore("buildingInfo", () => {
const floorList = ref([]); const floorList = ref([]);
const deptList = ref([]); const deptList = ref([]);
const mainSubSys = ref([]); const mainSubSys = ref([]);
const sysConfig = ref([]);
// 計算屬性 // 計算屬性
const mainSys = computed(() => const mainSys = computed(() =>
@ -47,9 +49,10 @@ const useBuildingStore = defineStore("buildingInfo", () => {
const fetchBuildings = async () => { const fetchBuildings = async () => {
const res = await getBuildings(); const res = await getBuildings();
buildings.value = res.data; buildings.value = res.data;
if (res.data.length > 0 && !selectedBuilding.value) { if (res.data.length > 0 ) {
const storedBuilding = JSON.parse(localStorage.getItem("CviBuilding")); selectedBuilding.value = res.data[0]; // 預設選第一個建築
selectedBuilding.value = storedBuilding || res.data[0]; // 預設選第一個建築 }else{
selectedBuilding.value = null; // 如果沒有建築物則設為null
} }
}; };
@ -81,13 +84,21 @@ const useBuildingStore = defineStore("buildingInfo", () => {
mainSubSys.value = res.data.history_Main_Systems; mainSubSys.value = res.data.history_Main_Systems;
}; };
// 取得系統設定
const getSysConfig = async (building_guid) => {
const res = await getSystemConfig(building_guid);
sysConfig.value = res.data;
};
// 當 selectedBuilding 改變時,更新 floorList 和 deptList 和 mainSubSys // 當 selectedBuilding 改變時,更新 floorList 和 deptList 和 mainSubSys
watch(selectedBuilding, async (newBuilding) => { watch(selectedBuilding, async (newBuilding) => {
if (newBuilding) { if (newBuilding) {
localStorage.setItem("EmpowerBuilding", JSON.stringify(newBuilding));
await Promise.all([ await Promise.all([
fetchFloorList(newBuilding.building_guid), fetchFloorList(newBuilding.building_guid),
fetchDepartmentList(), fetchDepartmentList(),
getSubMonitorPage(newBuilding.building_guid) getSubMonitorPage(newBuilding.building_guid),
getSysConfig(newBuilding.building_guid),
]); ]);
} }
}); });
@ -106,9 +117,11 @@ const useBuildingStore = defineStore("buildingInfo", () => {
mainSys, mainSys,
subSys, subSys,
selectedSystem, selectedSystem,
sysConfig,
fetchBuildings, fetchBuildings,
fetchFloorList, fetchFloorList,
fetchDepartmentList, fetchDepartmentList,
getSubMonitorPage,
initialize, initialize,
}; };
}); });

View File

@ -12,11 +12,23 @@ const instance = axios.create({
instance.interceptors.request.use( instance.interceptors.request.use(
function (config) { function (config) {
// Do something before request is sent // Do something before request is sent
const token = useGetCookie("JWT-Authorization"); const token = useGetCookie("JWT-Authorization");
config.headers = { // 取得 building_guid 並加到 headers
Authorization: `Bearer ${token}`, let buildingGuid = "";
}; try {
return config; const cviBuilding = localStorage.getItem("EmpowerBuilding");
if (cviBuilding) {
const parsed = JSON.parse(cviBuilding);
buildingGuid = parsed.building_guid || "";
}
} catch (e) {
buildingGuid = "";
}
config.headers = {
Authorization: `Bearer ${token}`,
"X-Building-GUID": buildingGuid,
};
return config;
}, },
function (error) { function (error) {
// Do something with request error // Do something with request error
@ -55,11 +67,23 @@ export const fileInstance = axios.create({
fileInstance.interceptors.request.use( fileInstance.interceptors.request.use(
function (config) { function (config) {
// Do something before request is sent // Do something before request is sent
const token = useGetCookie("JWT-Authorization"); const token = useGetCookie("JWT-Authorization");
config.headers = { // 取得 building_guid 並加到 headers
Authorization: `Bearer ${token}`, let buildingGuid = "";
}; try {
return config; const cviBuilding = localStorage.getItem("EmpowerBuilding");
if (cviBuilding) {
const parsed = JSON.parse(cviBuilding);
buildingGuid = parsed.building_guid || "";
}
} catch (e) {
buildingGuid = "";
}
config.headers = {
Authorization: `Bearer ${token}`,
"X-Building-GUID": buildingGuid,
};
return config;
}, },
function (error) { function (error) {
// Do something with request error // Do something with request error

View File

@ -51,6 +51,7 @@ const formState = ref({
system_value: "", system_value: "",
system_parent_id: 0, system_parent_id: 0,
file: [], file: [],
device_image: "",
}); });
@ -68,6 +69,7 @@ const openModal = (item) => {
} }
: {}; : {};
formState.value.file = [subFile]; formState.value.file = [subFile];
formState.value.device_image = item.device_image;
} }
} else { } else {
formState.value = { formState.value = {
@ -76,6 +78,7 @@ const openModal = (item) => {
system_value: "", system_value: "",
system_parent_id: 0, system_parent_id: 0,
file: [], file: [],
device_image: "",
}; };
} }
asset_add_sub_item.showModal(); asset_add_sub_item.showModal();

View File

@ -20,6 +20,7 @@ const mainSystem = ref([]);
const updateFileList = (files) => { const updateFileList = (files) => {
console.log("file", files); console.log("file", files);
props.formState.file = files; props.formState.file = files;
props.formState.device_image = files[0]?.name;
}; };
const getMainSystems = async () => { const getMainSystems = async () => {
@ -44,6 +45,7 @@ const onCancel = () => {
system_value: "", system_value: "",
system_parent_id: 0, system_parent_id: 0,
file: [], file: [],
device_image: "",
}; };
asset_add_sub_item.close(); asset_add_sub_item.close();
updateFileList([]); updateFileList([]);
@ -71,16 +73,15 @@ const onOk = async () => {
if (props.formState.file[0]) { if (props.formState.file[0]) {
formData.append("file", props.formState.file[0]); formData.append("file", props.formState.file[0]);
} formData.append("device_image", props.formState.device_image);
if (props.formState.Device_image) {
formData.append("Device_image", props.formState.Device_image);
} }
// const res = await postAssetSubList(formData); const res = await postAssetSubList(formData);
// if (res.isSuccess) { if (res.isSuccess) {
// props.getData(parseInt(searchParams.value.mainSys_id)); props.getData(parseInt(searchParams.value.mainSys_id));
// onCancel(); store.getSubMonitorPage(store.selectedBuilding.building_guid);
// } onCancel();
}
}; };
</script> </script>
@ -101,7 +102,7 @@ const onOk = async () => {
> >
<template #modalContent> <template #modalContent>
<form ref="form" class="mt-5 flex flex-col items-center"> <form ref="form" class="mt-5 flex flex-col items-center">
<Input name="system_key" :value="formState" > <Input name="system_key" :value="formState">
<template #topLeft>{{ $t("assetManagement.system_name") }}</template> <template #topLeft>{{ $t("assetManagement.system_name") }}</template>
<template #bottomLeft <template #bottomLeft
><span class="text-error text-base"> ><span class="text-error text-base">

View File

@ -2,7 +2,7 @@
import AlertSearch from "./AlertSearch.vue"; import AlertSearch from "./AlertSearch.vue";
import AlertTable from "./AlertTable.vue"; import AlertTable from "./AlertTable.vue";
import AlertTableModal from "./AlertTableModal.vue"; import AlertTableModal from "./AlertTableModal.vue";
import { ref, provide, onMounted } from "vue"; import { ref, provide, onMounted, watch } from "vue";
import useSearchParam from "@/hooks/useSearchParam"; import useSearchParam from "@/hooks/useSearchParam";
import { getAlertLog } from "@/apis/alert"; import { getAlertLog } from "@/apis/alert";
import { import {
@ -15,7 +15,7 @@ import useBuildingStore from "@/stores/useBuildingStore";
const { searchParams } = useSearchParam(); const { searchParams } = useSearchParam();
const store = useBuildingStore(); const store = useBuildingStore();
const hasSearched = ref(false);
const tableLoading = ref(false); const tableLoading = ref(false);
const dataSource = ref([]); const dataSource = ref([]);
const model_data = ref({ const model_data = ref({
@ -82,7 +82,7 @@ const openModal = async (record) => {
updateEditRecord({ updateEditRecord({
...res.data, ...res.data,
uuid: res.data.error_code, uuid: res.data.error_code,
device_number: record.device_number device_number: record.device_number,
}); });
} else { } else {
updateEditRecord({ updateEditRecord({
@ -98,11 +98,31 @@ const openModal = async (record) => {
onMounted(() => { onMounted(() => {
getAllOptions(); getAllOptions();
if (Object.keys(searchParams.value).length !== 0) {
search();
}
}); });
watch(
() => ({
isRecovery: searchParams.value.isRecovery,
Start_date: searchParams.value.Start_date,
End_date: searchParams.value.End_date,
device_name_tag: searchParams.value.device_name_tag,
}),
(val) => {
//
if (
val.isRecovery !== undefined &&
val.Start_date &&
val.End_date &&
val.device_name_tag &&
!hasSearched.value
) {
hasSearched.value = true;
search();
}
},
{ immediate: true, deep: true }
);
provide("alert_modal", { model_data, search, updateEditRecord }); provide("alert_modal", { model_data, search, updateEditRecord });
provide("alert_table", { provide("alert_table", {
openModal, openModal,

View File

@ -26,10 +26,6 @@ onMounted(() => {
initializeItems(); initializeItems();
}); });
watch(locale, () => {
initializeItems();
});
// //
watch( watch(
selectedBtn, selectedBtn,

View File

@ -12,7 +12,7 @@ const changeCheckedItem = () => {
if (checkedItem.value.length === store.subSys.length) { if (checkedItem.value.length === store.subSys.length) {
changeParams({ changeParams({
...searchParams.value, ...searchParams.value,
device_name_tag: [], device_name_tag: [store.subSys[0]?.sub_system_tag],
}); });
} else { } else {
changeParams({ changeParams({
@ -50,11 +50,12 @@ const checkedItem = computed(() =>
: [] : []
); );
//
watch(searchParams, (newValue) => { watch(searchParams, (newValue) => {
if (!newValue.device_name_tag) { if (!newValue.device_name_tag) {
changeParams({ changeParams({
...newValue, ...newValue,
device_name_tag: [store.subSys[0]?.sub_system_tag], device_name_tag: store.subSys.map(({ sub_system_tag }) => sub_system_tag),
}); });
} }
}); });

View File

@ -2,7 +2,10 @@
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 DashboardProduct from "./components/DashboardProduct.vue";
import DashboardProductComplete from "./components/DashboardProductComplete.vue";
import DashboardTemp from "./components/DashboardTemp.vue"; import DashboardTemp from "./components/DashboardTemp.vue";
import DashboardHumidity from "./components/DashboardHumidity.vue";
import DashboardRefrigTemp from "./components/DashboardRefrigTemp.vue"; import DashboardRefrigTemp from "./components/DashboardRefrigTemp.vue";
import DashboardIndoorTemp from "./components/DashboardIndoorTemp.vue"; import DashboardIndoorTemp from "./components/DashboardIndoorTemp.vue";
import DashboardElectricity from "./components/DashboardElectricity.vue"; import DashboardElectricity from "./components/DashboardElectricity.vue";
@ -111,10 +114,16 @@ onUnmounted(() => {
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>
<DashboardProduct />
</div>
<div class="mt-6">
<DashboardProductComplete />
</div>
<div class="mt-6">
<DashboardTemp /> <DashboardTemp />
</div> </div>
<div class="mt-10"> <div class="mt-10">
<DashboardAlert /> <DashboardHumidity />
</div> </div>
</div> </div>
<div <div
@ -135,6 +144,9 @@ onUnmounted(() => {
<div class="mt-10"> <div class="mt-10">
<DashboardEmission /> <DashboardEmission />
</div> </div>
<div class="mt-10">
<DashboardAlert />
</div>
</div> </div>
</div> </div>
</template> </template>

View File

@ -9,12 +9,16 @@ let intervalId = null; // 用來儲存 setInterval 的 ID
const getAlarmData = async (building_guid) => { const getAlarmData = async (building_guid) => {
const res = await getAlertLogList(building_guid); const res = await getAlertLogList(building_guid);
dataSource.value = (res.data || []); dataSource.value = res.data || [];
}; };
watch( watch(
() => store.selectedBuilding, () => store.selectedBuilding,
(newBuilding) => { (newBuilding) => {
if (intervalId) {
clearInterval(intervalId);
intervalId = null;
}
if (newBuilding) { if (newBuilding) {
getAlarmData(newBuilding.building_guid); getAlarmData(newBuilding.building_guid);
@ -38,7 +42,7 @@ onUnmounted(() => {
<h3 class="text-info text-xl text-center"> <h3 class="text-info text-xl text-center">
{{ $t("dashboard.alerts_data") }} Top 5 {{ $t("dashboard.alerts_data") }} Top 5
</h3> </h3>
<div class="overflow-x-auto"> <div class="overflow-x-auto min-h-[300px]">
<table class="table"> <table class="table">
<thead> <thead>
<tr class="border-b-2 border-info text-base"> <tr class="border-b-2 border-info text-base">

View File

@ -41,7 +41,7 @@ const defaultOption = (map, data = []) => {
}, },
map, map,
roam: true, // roam: true, //
layoutSize: window.innerWidth <= 768 ? "110%" : "100%", layoutSize: window.innerWidth <= 768 ? "110%" : "80%",
layoutCenter: ["50%", "50%"], layoutCenter: ["50%", "50%"],
scaleLimit: { min: 1, max: 2 }, scaleLimit: { min: 1, max: 2 },
}, },

View File

@ -72,7 +72,7 @@ const onCancel = () => {
<template> <template>
<button <button
class="btn btn-sm btn-success absolute top-0 right-0" class="btn btn-xs btn-success absolute top-0 right-0"
@click.stop.prevent="openModal" @click.stop.prevent="openModal"
> >
{{ $t("button.edit") }} {{ $t("button.edit") }}

View File

@ -67,7 +67,7 @@ const closeModal = () => {
</script> </script>
<template> <template>
<button class="btn btn-sm btn-success absolute top-0 right-0" @click.stop.prevent="openModal"> <button class="btn btn-xs btn-success absolute top-0 right-0" @click.stop.prevent="openModal">
{{ $t("button.edit") }} {{ $t("button.edit") }}
</button> </button>
<Modal <Modal

View File

@ -0,0 +1,213 @@
<script setup>
import LineChart from "@/components/chart/LineChart.vue";
import { SECOND_CHART_COLOR } from "@/constant";
import dayjs from "dayjs";
import { ref, watch, onUnmounted } from "vue";
import useActiveBtn from "@/hooks/useActiveBtn";
import { getDashboardTemp } from "@/apis/dashboard";
import useSearchParams from "@/hooks/useSearchParam";
import useBuildingStore from "@/stores/useBuildingStore";
const { searchParams } = useSearchParams();
const buildingStore = useBuildingStore();
const intervalType = "immediateTemp";
const timeoutTimer = ref("");
const { items, changeActiveBtn, setItems, selectedBtn } = useActiveBtn();
const data = ref([]);
const other_real_temp_chart = ref(null);
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",
},
},
series: [],
});
const getData = async (tempOption) => {
const res = await getDashboardTemp({
building_guid: buildingStore.selectedBuilding.building_guid,
tempOption, // 1 2:
timeInterval: 1, // =>1.4.8
});
if (res.isSuccess) {
if (tempOption === 1) {
console.log("室內溫度資料:", res.data["室溫"]);
data.value = res.data["室溫"] || [];
} else {
console.log("冷藏溫度資料:", res.data["冷藏溫度"]);
data.value = res.data["冷藏溫度"] || [];
}
}
};
//
watch(
() => buildingStore.sysConfig,
(newValue) => {
if (newValue) {
// sysConfig
const itemsArr = [];
if (buildingStore.sysConfig?.show_room) {
itemsArr.push({
title: "室內溫度",
key: 1,
active: false,
});
}
if (buildingStore.sysConfig?.show_refrigeration) {
itemsArr.push({
title: "冷藏溫度",
key: 2,
active: false,
});
}
if (itemsArr.length > 0) {
itemsArr[0].active = true;
// getData(itemsArr[0].key);
// timeoutTimer.value = setInterval(() => {
// getData(itemsArr[0].key);
// }, 60 * 1000);
}
setItems(itemsArr);
} else {
//
if (timeoutTimer.value) {
clearInterval(timeoutTimer.value);
}
}
},
{
immediate: true,
}
);
watch(
selectedBtn,
(newValue) => {
if (timeoutTimer.value) {
clearInterval(timeoutTimer.value);
}
if (newValue?.key) {
getData(newValue.key);
//
timeoutTimer.value = setInterval(() => {
getData(newValue.key);
}, 60 * 1000);
}
},
{
immediate: true,
deep: true,
}
);
watch(
data,
(newValue) => {
if (newValue?.length > 0 && other_real_temp_chart.value?.chart) {
const firstItem = newValue[0];
if (firstItem?.data?.length > 0) {
const validData = firstItem.data.filter(
(item) => item.value !== null && item.value !== undefined
);
if (validData.length > 0) {
const minValue = Math.min(...validData.map(({ value }) => value));
const maxValue = Math.max(...validData.map(({ value }) => value));
other_real_temp_chart.value.chart.setOption({
legend: {
data: newValue.map(({ full_name }) => full_name),
},
xAxis: {
data: firstItem.data.map(({ time }) =>
dayjs(time).format("HH:mm:ss")
),
},
yAxis: {
min: Math.floor(minValue),
max: Math.ceil(maxValue),
},
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 % SECOND_CHART_COLOR.length],
},
})),
});
}
}
}
},
{ deep: true }
);
onUnmounted(() => {
//
if (timeoutTimer.value) {
clearInterval(timeoutTimer.value);
}
});
</script>
<template>
<h3 class="text-info text-xl text-center">濕度趨勢</h3>
<div className="my-3 w-full flex justify-center relative">
<ButtonConnectedGroup
:items="items"
:onclick="
(e, item) => {
changeActiveBtn(item);
}
"
/>
</div>
<LineChart
id="dashboard_other_real_temp"
class="min-h-[260px] max-h-fit"
:option="defaultChartOption"
ref="other_real_temp_chart"
/>
</template>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,153 @@
<script setup>
import { ref, onMounted } from "vue";
import GaugeChart from "@/components/chart/GaugeChart.vue";
import { CHART_COLOR, SECOND_CHART_COLOR } from "@/constant";
const sameOptionAttr = {
type: "gauge",
center: ["50%", "60%"],
startAngle: 90,
endAngle: -270,
axisTick: {
show: false,
},
splitLine: {
show: false,
},
axisLabel: {
show: false,
},
anchor: {
show: false,
},
title: {
show: false,
},
};
const defaultChartOption = (text, lightColor, darkColor, value = 0) => ({
title: {
text,
left: "center",
textStyle: {
color: "#ffffff",
fontSize: 16,
fontWeight: "normal",
},
top: "0%",
},
series: [
{
...sameOptionAttr,
min: 0,
max: 100,
itemStyle: {
color: lightColor,
},
progress: {
show: true,
width: 10,
},
axisLine: {
lineStyle: {
width: 10,
},
},
pointer: {
itemStyle: {
color: darkColor,
},
},
detail: {
valueAnimation: true,
formatter: "{value} %",
fontSize: 16,
color: "#ffffff",
},
data: [
{
value,
},
],
},
{
...sameOptionAttr,
min: 0,
max: 100,
itemStyle: {
color: darkColor,
},
progress: {
show: true,
width: 4,
},
axisLine: {
show: false,
},
pointer: {
show: false,
},
detail: {
show: false,
},
data: [
{
value,
},
],
},
],
});
const mesona_chart = ref(null);
const aiyu_chart = ref(null);
const blackTea_chart = ref(null);
const chart_data = ref([
defaultChartOption("產品 A", CHART_COLOR[0], SECOND_CHART_COLOR[0], 75),
defaultChartOption("產品 B", CHART_COLOR[1], SECOND_CHART_COLOR[1], 60),
defaultChartOption("產品 C", CHART_COLOR[2], SECOND_CHART_COLOR[2], 90),
]);
onMounted(() => {
//
// setTimeout(() => {
// chart_data.value = [
// defaultChartOption("", CHART_COLOR[0], SECOND_CHART_COLOR[0], 80),
// defaultChartOption("", CHART_COLOR[1], SECOND_CHART_COLOR[1], 70),
// defaultChartOption("", CHART_COLOR[2], SECOND_CHART_COLOR[2], 95),
// ];
// }, 2000);
});
</script>
<template>
<div>
<h3 class="text-info text-xl text-center mb-3">生產量</h3>
<div class="w-full grid grid-cols-3">
<div>
<GaugeChart
id="dashboard_mesona_production"
class="h-32"
:option="chart_data[0]"
ref="mesona_chart"
/>
</div>
<div>
<GaugeChart
id="dashboard_aiyu_production"
class="h-32"
:option="chart_data[1]"
ref="aiyu_chart"
/>
</div>
<div>
<GaugeChart
id="dashboard_blackTea_production"
class="h-32"
:option="chart_data[2]"
ref="blackTea_chart"
/>
</div>
</div>
</div>
</template>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,95 @@
<script setup>
import useActiveBtn from "@/hooks/useActiveBtn";
import { ref, onMounted } from "vue";
import DashboardProductCompleteModal from "./DashboardProductCompleteModal.vue";
const { items, changeActiveBtn, setItems, selectedBtn } = useActiveBtn();
//
const progress_data = [
{
data: [
{ pac: "130g", value: 80, target: 100, percentage: 80 },
{ pac: "150g", value: 70, target: 100, percentage: 70 },
{ pac: "1kg", value: 75, target: 100, percentage: 75 },
],
},
{
data: [
{ pac: "500g", value: 70, target: 100, percentage: 70 },
{ pac: "1kg", value: 60, target: 100, percentage: 60 },
{ pac: "2kg", value: 50, target: 100, percentage: 50 },
],
},
{
data: [
{ pac: "300g", value: 90, target: 100, percentage: 90 },
{ pac: "500g", value: 96, target: 100, percentage: 96 },
{ pac: "1kg", value: 84, target: 100, percentage: 84 },
],
},
];
const openModal = () => {
dashboard_product.showModal();
};
onMounted(() => {
//
setItems([
{ title: "產品 A", key: 1, active: true },
{ title: "產品 B", key: 2, active: false },
{ title: "產品 C", key: 3, active: false },
]);
});
</script>
<template>
<DashboardProductCompleteModal/>
<div class="mb-3 relative">
<h3 class="text-info text-xl text-center">今日生產完成率 </h3>
<button
type="button"
class="btn btn-xs btn-success absolute top-0 right-0"
@click.stop="openModal"
>
設定
</button>
</div>
<div className="my-3 w-full flex justify-center relative">
<ButtonConnectedGroup
:items="items"
:onclick="
(e, item) => {
changeActiveBtn(item);
}
"
/>
</div>
<template
v-if="progress_data && selectedBtn && progress_data[selectedBtn.key - 1]"
>
<div
class="grid grid-cols-6 gap-3 justify-items-end items-center"
v-for="{ pac, value, target, percentage } in progress_data[
selectedBtn.key - 1
].data"
:key="pac"
>
<span class="col-span-1 text-lg">{{ pac }}</span>
<progress
v-if="target !== 0"
class="progress progress-info col-span-4"
:value="value"
:max="target"
></progress>
<span class="col-span-1 text-lg justify-self-start"
>{{ percentage }} %</span
>
</div>
</template>
<template v-else>
<p class="text-center mt-8 text-lg">尚未設定目標值</p>
</template>
</template>
<style lang="css" scoped></style>

View File

@ -0,0 +1,104 @@
<script setup>
import { ref, inject, onBeforeMount, defineProps, watch, computed } from "vue";
import DashboardProductCompleteModalTarget from "./DashboardProductCompleteModalTarget.vue";
import DashboardProductCompleteModalRecord from "./DashboardProductCompleteModalRecord.vue";
import useActiveBtn from "@/hooks/useActiveBtn";
import dayjs from "dayjs";
import { postDashboardProductTarget } from "@/apis/dashboard";
const { items, changeActiveBtn, setItems } = useActiveBtn();
const date = ref([]);
const formState = ref({});
const selectedBtn = ref({});
const onOk = async () => {
const res = await postDashboardProductTarget({
date: dayjs(date.value[0].value).format("YYYY-MM-DD"),
type: selectedBtn.value?.typeOption || 1,
data: formState.value,
});
if (res.isSuccess) {
// getCompletion();
onCancel();
}
};
const onCancel = () => {
dashboard_product.close();
};
const changeComponent = (e, item) => {
changeActiveBtn(item);
};
onBeforeMount(() => {
setItems([
{
title: "目標設定",
key: "Target",
active: true,
component: DashboardProductCompleteModalTarget,
},
{
title: "執行紀錄",
key: "Record",
active: false,
component: DashboardProductCompleteModalRecord,
},
]);
});
const activeTab = computed(() => {
return items.value.find(({ active }) => active);
});
</script>
<template>
<Modal
id="dashboard_product"
title="顯示設定"
:onCancel="onCancel"
width="1000"
modalClass="bg-body"
>
<template #modalContent>
<ButtonGroup
:items="items"
:withLine="true"
class="mt-8 mb-6"
:onclick="changeComponent"
/>
<component :is="activeTab.component"></component>
</template>
<template #modalAction>
<div v-if="activeTab.key === 'Target'">
<button
type="reset"
class="btn btn-outline-success mr-2"
@click.prevent="onCancel"
>
取消
</button>
<button
type="submit"
class="btn btn-outline-success"
@click.stop.prevent="onOk"
>
確定
</button>
</div>
<div v-else="activeTab.key === 'Record'">
<button
type="reset"
class="btn btn-outline-success mr-2"
@click.prevent="onCancel"
>
取消
</button>
</div>
</template>
</Modal>
</template>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,125 @@
<script setup>
import { ref, computed, onMounted } from "vue";
import dayjs from "dayjs";
import useActiveBtn from "@/hooks/useActiveBtn";
import {
getDashboardProductRecord,
} from "@/apis/dashboard";
const tableLoading = ref(false);
const dataSource = ref([]);
const {
items: dateRange,
setItems: setDateItems,
} = useActiveBtn();
const columns = [
{
key: "date",
title: "日期",
},
{
key: "full_name",
title: "項目",
width: 80,
},
{
key: "device_value",
title: "實際生產量(L)",
},
{
key: "target_value",
title: "目標生產量(L)",
},
{
key: "f2_value",
title: "二廠實際包裝數",
},
{
key: "f3_value",
title: "三廠實際包裝數",
},
{
key: "target",
title: "目標包裝數",
},
];
const submitBtns = computed(() => [
{
title: "查詢",
key: "search",
active: true,
onClick: getRecord,
disabled: tableLoading.value,
},
]);
const getRecord = async () => {
tableLoading.value = true;
try {
const res = await getDashboardProductRecord({
start_time: dayjs(dateRange.value[0].value).format("YYYY-MM-DD"),
end_time: dayjs(dateRange.value[1].value).format("YYYY-MM-DD"),
});
dataSource.value = res.data;
} catch (error) {
console.error("Error fetching data:", error);
} finally {
tableLoading.value = false;
}
};
onMounted(() => {
setDateItems([
{
key: "start_time",
value: dayjs(),
dateFormat: "yyyy-MM-dd",
placeholder: "起始日期",
},
{
key: "end_time",
value: dayjs(),
dateFormat: "yyyy-MM-dd",
placeholder: "結束時間",
},
]);
});
</script>
<template>
<div class="flex flex-row items-end">
<DateGroup :items="dateRange" :withLine="true" width="500">
<template #topLeft>日期</template>
</DateGroup>
<ButtonGroup :items="submitBtns" :withLine="false" class="ml-8 mr-8 mb-4" />
</div>
<Table :loading="tableLoading" :columns="columns" :dataSource="dataSource">
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'f2_value'">
<div v-for="(item, index) in record.data" :key="index">
<p class="text-start">{{ item.pac }} : {{ item.f2_value }}</p>
</div>
</template>
<template v-else-if="column.key === 'f3_value'">
<div v-for="(item, index) in record.data" :key="index">
<p class="text-start">{{ item.pac }} : {{ item.f3_value }}</p>
</div>
</template>
<template v-else-if="column.key === 'target'">
<div v-for="(item, index) in record.data" :key="index">
<p class="text-start">{{ item.pac }} : {{ item.target }}</p>
</div>
</template>
<template v-else>
{{ record[column.key] }}
</template>
</template>
</Table>
</template>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,74 @@
<script setup>
import { ref, inject, onMounted, defineProps, watch, watchEffect } from "vue";
import dayjs from "dayjs";
import useActiveBtn from "@/hooks/useActiveBtn";
const { items, changeActiveBtn, setItems, selectedBtn } = useActiveBtn();
const formState = ref({
pot: 0,
"130g": 0,
"150g": 0,
"1kg": 0,
"2kg": 0,
});
const date = ref([
{
key: "date",
value: dayjs(),
dateFormat: "yyyy-MM-dd",
placeholder: "日期設定",
},
]);
onMounted(() => {
setItems([
{ title: "產品 A", key: 1, active: true },
{ title: "產品 B", key: 2, active: false },
{ title: "產品 C", key: 3, active: false },
]);
});
</script>
<template>
<DateGroup :isBottomLabelExist="false" :items="date" :withLine="false">
<template #topLeft>日期</template>
</DateGroup>
<ButtonConnectedGroup
:items="items"
:className="`w-full mt-8 mb-4`"
size="md"
color="info"
:onclick="
(e, item) => {
changeActiveBtn(item);
}
"
/>
<div class="flex flex-col items-start">
<div class="flex justify-between">
<InputNumber :value="formState" width="150" class="mr-4" name="pot">
<template #topLeft>總數</template>
</InputNumber>
</div>
</div>
<div class="flex flex-col items-start">
<p class="text-base">包裝類型</p>
<div class="flex justify-between">
<InputNumber
v-for="size in ['130g', '150g', '1kg', '2kg']"
:key="size"
:value="formState"
width="150"
class="mr-4"
:name="size"
>
<template #topLeft>{{ size }}</template>
</InputNumber>
</div>
</div>
</template>
<style lang="scss" scoped></style>

View File

@ -77,32 +77,40 @@ const getData = async (tempOption) => {
// //
watch( watch(
() => buildingStore.selectedBuilding?.building_guid, () => buildingStore.sysConfig,
(newBuildingGuid) => { (newValue) => {
if (newBuildingGuid) { if (newValue) {
getData(1); // sysConfig
timeoutTimer.value = setInterval(() => { const itemsArr = [];
getData(1); if (buildingStore.sysConfig?.show_room) {
}, 60 * 1000); itemsArr.push({
title: "室內溫度",
key: 1,
active: false,
});
}
if (buildingStore.sysConfig?.show_refrigeration) {
itemsArr.push({
title: "冷藏溫度",
key: 2,
active: false,
});
}
if (itemsArr.length > 0) {
itemsArr[0].active = true;
// getData(itemsArr[0].key);
// timeoutTimer.value = setInterval(() => {
// getData(itemsArr[0].key);
// }, 60 * 1000);
}
setItems(itemsArr);
} else { } else {
// //
if (timeoutTimer.value) { if (timeoutTimer.value) {
clearInterval(timeoutTimer.value); clearInterval(timeoutTimer.value);
} }
} }
setItems([
{
title: "室內溫度",
key: 1,
active: true,
},
{
title: "冷藏溫度",
key: 2,
active: false,
},
]);
}, },
{ {
immediate: true, immediate: true,
@ -115,7 +123,7 @@ watch(
if (timeoutTimer.value) { if (timeoutTimer.value) {
clearInterval(timeoutTimer.value); clearInterval(timeoutTimer.value);
} }
if (newValue?.key) { if (newValue?.key) {
getData(newValue.key); getData(newValue.key);
// //
@ -125,6 +133,7 @@ watch(
} }
}, },
{ {
immediate: true,
deep: true, deep: true,
} }
); );
@ -135,18 +144,20 @@ watch(
if (newValue?.length > 0 && other_real_temp_chart.value?.chart) { if (newValue?.length > 0 && other_real_temp_chart.value?.chart) {
const firstItem = newValue[0]; const firstItem = newValue[0];
if (firstItem?.data?.length > 0) { if (firstItem?.data?.length > 0) {
const validData = firstItem.data.filter((item) => item.value !== null && item.value !== undefined); const validData = firstItem.data.filter(
(item) => item.value !== null && item.value !== undefined
);
if (validData.length > 0) { if (validData.length > 0) {
const minValue = Math.min(...validData.map(({ value }) => value)); const minValue = Math.min(...validData.map(({ value }) => value));
const maxValue = Math.max(...validData.map(({ value }) => value)); const maxValue = Math.max(...validData.map(({ value }) => value));
other_real_temp_chart.value.chart.setOption({ other_real_temp_chart.value.chart.setOption({
legend: { legend: {
data: newValue.map(({ full_name }) => full_name), data: newValue.map(({ full_name }) => full_name),
}, },
xAxis: { xAxis: {
data: firstItem.data.map(({ time }) => time), // 使 time data: firstItem.data.map(({ time }) => dayjs(time).format("HH:mm:ss")),
}, },
yAxis: { yAxis: {
min: Math.floor(minValue), min: Math.floor(minValue),
@ -178,23 +189,20 @@ onUnmounted(() => {
</script> </script>
<template> <template>
<div class="flex flex-col justify-center mb-3"> <h3 class="text-info text-xl text-center">溫度趨勢</h3>
<h3 class="text-info font-bold text-xl text-center">溫度趨勢</h3> <div className="my-3 w-full flex justify-center relative">
<div className="mt-2 w-full flex justify-center relative"> <ButtonConnectedGroup
<ButtonConnectedGroup :items="items"
:items="items" :onclick="
:onclick=" (e, item) => {
(e, item) => { changeActiveBtn(item);
changeActiveBtn(item); }
} "
" />
/>
</div>
</div> </div>
<LineChart <LineChart
id="dashboard_other_real_temp" id="dashboard_other_real_temp"
class="min-h-[350px] max-h-fit" class="min-h-[260px] max-h-fit"
:option="defaultChartOption" :option="defaultChartOption"
ref="other_real_temp_chart" ref="other_real_temp_chart"
/> />

View File

@ -24,29 +24,34 @@ watch(
item.costPeak + item.costHalfPeak + item.costOffPeak + item.costBase; item.costPeak + item.costHalfPeak + item.costOffPeak + item.costBase;
return sum + monthlyBill; return sum + monthlyBill;
}, 0) }, 0)
.toFixed(2); .toFixed(2)
.replace(/\B(?=(\d{3})+(?!\d))/g, ",");
totalElecDegree.value = newData totalElecDegree.value = newData
.reduce((sum, item) => { .reduce((sum, item) => {
const monthlyBill = const monthlyBill =
item.degreePeak + item.degreeHalfPeak + item.degreeOffPeak; item.degreePeak + item.degreeHalfPeak + item.degreeOffPeak;
return sum + monthlyBill; return sum + monthlyBill;
}, 0) }, 0)
.toFixed(2); .toFixed(2)
.replace(/\B(?=(\d{3})+(?!\d))/g, ",");
// //
const latestData = newData[newData.length - 1]; const latestData = newData[newData.length - 1];
IntervalElecBills.value = ( IntervalElecBills.value = (
latestData.costPeak + latestData.costPeak +
latestData.costHalfPeak + latestData.costHalfPeak +
latestData.costOffPeak + latestData.costOffPeak +
latestData.costBase latestData.costBase
).toFixed(2); )
.toFixed(2)
.replace(/\B(?=(\d{3})+(?!\d))/g, ",");
IntervalElecDegree.value = ( IntervalElecDegree.value = (
latestData.degreePeak + latestData.degreePeak +
latestData.degreeHalfPeak + latestData.degreeHalfPeak +
latestData.degreeOffPeak latestData.degreeOffPeak
).toFixed(2); )
.toFixed(2)
.replace(/\B(?=(\d{3})+(?!\d))/g, ",");
const monthDays = latestData.time ? daysInMonth(latestData.time) : 0; const monthDays = latestData.time ? daysInMonth(latestData.time) : 0;
IntervalDate.value = latestData.time IntervalDate.value = latestData.time
? `${latestData.time}-01~${latestData.time}-${monthDays}` ? `${latestData.time}-01~${latestData.time}-${monthDays}`

View File

@ -8,28 +8,28 @@ import { ref, provide } from "vue";
const tableData = ref([]); const tableData = ref([]);
const loading = ref(false); const loading = ref(false);
const updateTableData = (data) => { const updateTableData = (data) => {
tableData.value = data tableData.value = data ? data : [];
? data.items.map((d) => ({
...d,
key: `${d.device_number}_${d.points}_${dayjs(d.timestamp).format("x")}`,
}))
: [];
}; };
const updateLoading = () => { const updateLoading = () => {
loading.value = !loading.value; loading.value = !loading.value;
}; };
provide("history_table_data", { tableData, updateTableData, loading, updateLoading }); provide("history_table_data", {
tableData,
updateTableData,
loading,
updateLoading,
});
</script> </script>
<template> <template>
<h1 class="text-2xl font-extrabold mb-2">{{ $t('history.title') }}</h1> <h1 class="text-2xl font-extrabold mb-2">{{ $t("history.title") }}</h1>
<div class="grid grid-cols-10 gap-2"> <div class="grid grid-cols-10 gap-2">
<div class="col-span-10 lg:col-span-2 "> <div class="col-span-10 lg:col-span-2">
<HistorySidebar /> <HistorySidebar />
</div> </div>
<div class="col-span-10 lg:col-span-8 "> <div class="col-span-10 lg:col-span-8">
<HistorySearch /> <HistorySearch />
<HistoryTable /> <HistoryTable />
</div> </div>

View File

@ -8,7 +8,6 @@ const { searchParams } = useSearchParam();
const props = defineProps({ const props = defineProps({
form: Object, form: Object,
selectedType: String,
}); });
const { updateTableData, updateLoading } = inject("history_table_data"); const { updateTableData, updateLoading } = inject("history_table_data");
@ -53,6 +52,7 @@ const submit = async (e, type = "") => {
updateLoading(); updateLoading();
const res = await getHistoryData({ const res = await getHistoryData({
type: searchParams.value.selectedType, type: searchParams.value.selectedType,
table_type:1,
...params, ...params,
...searchParams.value, ...searchParams.value,
}); });
@ -118,13 +118,18 @@ watch(
</script> </script>
<template> <template>
<Toast <Toast
:content="isToastOpen.content" :content="isToastOpen.content"
:open="isToastOpen.open" :open="isToastOpen.open"
status="info" status="info"
:cancel="cancelToastOpen" :cancel="cancelToastOpen"
/> />
<ButtonGroup class="lg:ml-5" :items="submitBtns" :withLine="false" :withBtnClass="true"/> <ButtonGroup
class="lg:ml-5"
:items="submitBtns"
:withLine="false"
:withBtnClass="true"
/>
</template> </template>
<style lang="scss" scoped></style> <style lang="scss" scoped></style>

View File

@ -0,0 +1,164 @@
<script setup>
import { inject, computed, ref, watch } from "vue";
import { useI18n } from "vue-i18n";
import LineChart from "@/components/chart/LineChart.vue";
import { SECOND_CHART_COLOR } from "@/constant";
import useSearchParam from "@/hooks/useSearchParam";
import dayjs from "dayjs";
const { searchParams } = useSearchParam();
const { t } = useI18n();
const { tableData } = inject("history_table_data");
const history_chart = ref(null);
//
const defaultChartOption = {
tooltip: {
trigger: "axis",
},
legend: {
data: [],
textStyle: {
color: "#ffffff",
fontSize: 16,
},
},
grid: {
top: "25%",
left: "0%",
right: "0%",
bottom: "0%",
containLabel: true,
},
toolbox: {
show: true,
feature: {
saveAsImage: {
type: "png",
name: "Energy_management_chart",
backgroundColor: "rgba(29, 36, 44, 1)",
iconStyle: {
borderColor: "rgba(53,237,237, 1)",
},
},
},
},
xAxis: {
type: "category",
splitLine: { show: false },
axisLabel: {
color: "#ffffff",
formatter: (value) =>
searchParams.value.Type == 2
? dayjs(value).format("HH:mm")
: dayjs(value).format("MM-DD"), //
},
data: [],
},
yAxis: {
type: "value",
splitLine: { show: false },
axisLabel: { color: "#ffffff" },
// interval: 100, //Y
min: "dataMin",
max: "dataMax",
// splitArea: { show: false },
},
series: [],
};
//
const formatChartData = (data) => {
return data.reduce((acc, item) => {
const seriesKey = `${item.device_name || ""}_${item.item_name || ""}`;
acc[seriesKey] = {
timestamps: item.data.map((d) =>
dayjs(d.time).format("YYYY-MM-DD HH:mm")
),
values: item.data.map((d) =>
d.value == "無資料" ? null : parseFloat(d.value)
),
maxValue: parseFloat(item.maxValue),
minValue: parseFloat(item.minValue),
averageValue: parseFloat(item.averageValue),
};
return acc;
}, {});
};
// tableData
watch(
tableData,
(newData) => {
if (newData?.length > 0) {
const formattedData = formatChartData(newData);
const series = Object.keys(formattedData).map((seriesKey, index) => {
const { maxValue, minValue, averageValue } = formattedData[seriesKey];
return {
name: seriesKey,
type: "line",
data: formattedData[seriesKey].values,
showSymbol: false,
itemStyle: {
color: SECOND_CHART_COLOR[index % SECOND_CHART_COLOR.length],
},
markPoint: {
data: [
maxValue !== null
? { type: "max", name: "Max", value: maxValue }
: null,
minValue !== null
? { type: "min", name: "Min", value: minValue }
: null,
].filter(Boolean),
},
markLine: {
data:
averageValue !== null
? [
{
yAxis: averageValue,
name: "Avg",
},
]
: [],
},
};
});
//
history_chart.value?.chart.setOption(
{
...defaultChartOption,
legend: {
...defaultChartOption.legend,
data: Object.keys(formattedData),
},
xAxis: {
...defaultChartOption.xAxis,
data:
Object.keys(formattedData).length > 0
? formattedData[Object.keys(formattedData)[0]].timestamps
: [],
},
series,
},
true
);
}
},
{ deep: true }
);
</script>
<template>
<LineChart
id="history_chart"
class="min-h-[350px] max-h-fit"
:option="defaultChartOption"
ref="history_chart"
/>
</template>
<style lang="scss" scoped></style>

View File

@ -15,7 +15,11 @@ const searchTerm = ref(""); //搜尋文字
const activeSearchTerm = ref(""); const activeSearchTerm = ref("");
const getDeviceData = async (sub_system_tag, department_id) => { const getDeviceData = async (sub_system_tag, department_id) => {
const deptArray = department_id ? department_id.map(Number) : []; const deptArray = Array.isArray(department_id)
? department_id.map(Number)
: department_id
? [Number(department_id)]
: [];
const res = await getHistorySideBar({ const res = await getHistorySideBar({
sub_system_tag: sub_system_tag, sub_system_tag: sub_system_tag,
department_id: deptArray, department_id: deptArray,
@ -110,7 +114,7 @@ const checkedBuilding = computed(() => {
if ( if (
selectedDeviceNumber.value?.filter( selectedDeviceNumber.value?.filter(
(d) => d.split("_")[1] === building_tag (d) => d && d.split("_")[1] === building_tag // d &&
).length === allDevices.length ).length === allDevices.length
) { ) {
selected.push(building_tag); selected.push(building_tag);

View File

@ -1,14 +1,20 @@
<script setup> <script setup>
import Table from "@/components/customUI/Table.vue"; import Table from "@/components/customUI/Table.vue";
import { inject, computed } from "vue"; import HistoryDataCahrt from "./HistoryDataCahrt.vue";
import { ref, inject, computed, watch } from "vue";
import { useI18n } from "vue-i18n"; import { useI18n } from "vue-i18n";
const { t } = useI18n(); const { t } = useI18n();
const { tableData, loading } = inject("history_table_data");
const processedTableData = ref([]);
const columns = computed(() => [ const columns = computed(() => [
{ {
title: t("history.building_name"), title: t("assetManagement.department"),
key: "building_name", key: "department_name",
sort: true, },
{
title: t("energy.floor"),
key: "floor_name",
}, },
{ {
title: t("history.device_name"), title: t("history.device_name"),
@ -18,7 +24,7 @@ const columns = computed(() => [
sort: true, sort: true,
}, },
{ {
title: t("history.category"), title: t("assetManagement.point"),
key: "item_name", key: "item_name",
}, },
{ {
@ -35,11 +41,76 @@ const columns = computed(() => [
}, },
]); ]);
const { tableData, loading } = inject("history_table_data"); const columns2 = computed(() => [
{
title: t("assetManagement.department"),
key: "department_name",
},
{
title: t("energy.floor"),
key: "floor_name",
},
{
title: t("history.device_name"),
key: "device_name",
filter: true,
},
{
title: t("assetManagement.point"),
key: "item_name",
},
{
title: t("energy.maximum"),
key: "maxValue",
},
{
title: t("energy.maximum_time"),
key: "maxTime",
},
{
title: t("energy.minimum"),
key: "minValue",
},
{
title: t("energy.minimum_time"),
key: "minTime",
},
{
title: t("energy.average_value"),
key: "averageValue",
},
]);
//
const processData = (data) => {
if (!data || !Array.isArray(data) || data.length === 0) {
return [];
}
// data
return data.flatMap((item) =>
(item.data || []).map((d) => ({
...item,
value: d.value,
timestamp: d.time,
}))
);
};
watch(
tableData,
(newTableData) => {
processedTableData.value = processData(newTableData);
},
{ immediate: true } //
);
</script> </script>
<template> <template>
<Table :columns="columns" :dataSource="tableData" :loading="loading"></Table> <Table :columns="columns2" :dataSource="tableData" :loading="loading"
><template #beforeTable> <HistoryDataCahrt class="mb-10" /></template
></Table>
<Table :columns="columns" :dataSource="processedTableData" :loading="loading">
</Table>
</template> </template>
<style lang="scss" scoped></style> <style lang="scss" scoped></style>