預設繁體中文 | 拿掉右上告警icon | 新創賦能 : 告警初始化後全選+search | 歷史資料加最大最小值chart、table
This commit is contained in:
parent
c48072a41c
commit
6e61679c1f
@ -1,3 +1,5 @@
|
||||
export const GET_SYSTEM_FLOOR_LIST_API = `/api/Device/GetFloor`;
|
||||
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`;
|
@ -2,6 +2,7 @@ import {
|
||||
GET_SYSTEM_FLOOR_LIST_API,
|
||||
GET_SYSTEM_DEVICE_LIST_API,
|
||||
GET_SYSTEM_REALTIME_API,
|
||||
GET_SYSTEM_CONFIG_API
|
||||
} from "./api";
|
||||
import instance from "@/util/request";
|
||||
import apihandler from "@/util/apihandler";
|
||||
@ -36,3 +37,11 @@ export const getSystemRealTime = async (device_list) => {
|
||||
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,
|
||||
});
|
||||
};
|
||||
|
@ -121,9 +121,9 @@ watch(locale, () => {
|
||||
<li>
|
||||
<NavbarLang />
|
||||
</li>
|
||||
<li>
|
||||
<!-- <li>
|
||||
<AlarmDrawer />
|
||||
</li>
|
||||
</li> -->
|
||||
<li>
|
||||
<NavbarUser />
|
||||
</li>
|
||||
|
@ -6,7 +6,7 @@ const store = useBuildingStore();
|
||||
|
||||
const selectBuilding = (bui) => {
|
||||
store.selectedBuilding = bui; // 改變 selectedBuilding,watch 會自動更新資料
|
||||
localStorage.setItem("CviBuilding", JSON.stringify(bui));
|
||||
localStorage.setItem("EmpowerBuilding", JSON.stringify(bui));
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
|
@ -6,7 +6,7 @@ const { locale } = useI18n(); // 使用 I18n
|
||||
// 切換語言
|
||||
const toggleLanguage = (lang) => {
|
||||
locale.value = lang;
|
||||
localStorage.setItem("CviLanguage", lang);
|
||||
localStorage.setItem("EmpowerLanguage", lang);
|
||||
};
|
||||
</script>
|
||||
|
||||
|
@ -1,9 +1,16 @@
|
||||
<script setup>
|
||||
import useUserInfoStore from "@/stores/useUserInfoStore";
|
||||
import { ref, onMounted } from "vue";
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
const router = useRouter();
|
||||
const store = useUserInfoStore();
|
||||
const user = ref("");
|
||||
|
||||
function logout() {
|
||||
router.push('/logout');
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
const name = store.user.user_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"
|
||||
>
|
||||
<li class="text-white">
|
||||
<router-link
|
||||
to="logout"
|
||||
type="link"
|
||||
<a
|
||||
type="button"
|
||||
class="flex flex-col justify-center items-center"
|
||||
@click="logout"
|
||||
>{{ $t("sign_out") }}
|
||||
</router-link>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
@ -30,12 +30,12 @@ const messages = {
|
||||
us,
|
||||
};
|
||||
|
||||
const storedLanguage = localStorage.getItem("CviLanguage") || "us";
|
||||
const storedLanguage = localStorage.getItem("EmpowerLanguage") || "tw";
|
||||
|
||||
const i18n = createI18n({
|
||||
legacy: false,
|
||||
locale: storedLanguage,
|
||||
fallbackLocale: 'us',
|
||||
fallbackLocale: 'tw',
|
||||
messages,
|
||||
});
|
||||
const app = createApp(App);
|
||||
|
@ -106,6 +106,7 @@ router.beforeEach(async (to, from, next) => {
|
||||
document.cookie = "user_name=; Max-Age=0";
|
||||
auth.user.token = "";
|
||||
auth.user.user_name = "";
|
||||
localStorage.removeItem("EmpowerBuilding");
|
||||
window.location.reload();
|
||||
next({ path: "/login" });
|
||||
}
|
||||
|
@ -3,6 +3,7 @@ import { ref, computed, watch } from "vue";
|
||||
import { useRoute } from "vue-router";
|
||||
import { getBuildings, getAllSysSidebar } from "@/apis/building";
|
||||
import { getAssetFloorList, getDepartmentList } from "@/apis/asset";
|
||||
import { getSystemConfig } from "@/apis/system";
|
||||
|
||||
const useBuildingStore = defineStore("buildingInfo", () => {
|
||||
// 狀態定義
|
||||
@ -11,6 +12,7 @@ const useBuildingStore = defineStore("buildingInfo", () => {
|
||||
const floorList = ref([]);
|
||||
const deptList = ref([]);
|
||||
const mainSubSys = ref([]);
|
||||
const sysConfig = ref([]);
|
||||
|
||||
// 計算屬性
|
||||
const mainSys = computed(() =>
|
||||
@ -47,9 +49,10 @@ const useBuildingStore = defineStore("buildingInfo", () => {
|
||||
const fetchBuildings = async () => {
|
||||
const res = await getBuildings();
|
||||
buildings.value = res.data;
|
||||
if (res.data.length > 0 && !selectedBuilding.value) {
|
||||
const storedBuilding = JSON.parse(localStorage.getItem("CviBuilding"));
|
||||
selectedBuilding.value = storedBuilding || res.data[0]; // 預設選第一個建築
|
||||
if (res.data.length > 0 ) {
|
||||
selectedBuilding.value = res.data[0]; // 預設選第一個建築
|
||||
}else{
|
||||
selectedBuilding.value = null; // 如果沒有建築物,則設為null
|
||||
}
|
||||
};
|
||||
|
||||
@ -81,13 +84,21 @@ const useBuildingStore = defineStore("buildingInfo", () => {
|
||||
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
|
||||
watch(selectedBuilding, async (newBuilding) => {
|
||||
if (newBuilding) {
|
||||
localStorage.setItem("EmpowerBuilding", JSON.stringify(newBuilding));
|
||||
await Promise.all([
|
||||
fetchFloorList(newBuilding.building_guid),
|
||||
fetchDepartmentList(),
|
||||
getSubMonitorPage(newBuilding.building_guid)
|
||||
getSubMonitorPage(newBuilding.building_guid),
|
||||
getSysConfig(newBuilding.building_guid),
|
||||
]);
|
||||
}
|
||||
});
|
||||
@ -106,9 +117,11 @@ const useBuildingStore = defineStore("buildingInfo", () => {
|
||||
mainSys,
|
||||
subSys,
|
||||
selectedSystem,
|
||||
sysConfig,
|
||||
fetchBuildings,
|
||||
fetchFloorList,
|
||||
fetchDepartmentList,
|
||||
getSubMonitorPage,
|
||||
initialize,
|
||||
};
|
||||
});
|
||||
|
@ -12,11 +12,23 @@ const instance = axios.create({
|
||||
instance.interceptors.request.use(
|
||||
function (config) {
|
||||
// Do something before request is sent
|
||||
const token = useGetCookie("JWT-Authorization");
|
||||
config.headers = {
|
||||
Authorization: `Bearer ${token}`,
|
||||
};
|
||||
return config;
|
||||
const token = useGetCookie("JWT-Authorization");
|
||||
// 取得 building_guid 並加到 headers
|
||||
let buildingGuid = "";
|
||||
try {
|
||||
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) {
|
||||
// Do something with request error
|
||||
@ -55,11 +67,23 @@ export const fileInstance = axios.create({
|
||||
fileInstance.interceptors.request.use(
|
||||
function (config) {
|
||||
// Do something before request is sent
|
||||
const token = useGetCookie("JWT-Authorization");
|
||||
config.headers = {
|
||||
Authorization: `Bearer ${token}`,
|
||||
};
|
||||
return config;
|
||||
const token = useGetCookie("JWT-Authorization");
|
||||
// 取得 building_guid 並加到 headers
|
||||
let buildingGuid = "";
|
||||
try {
|
||||
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) {
|
||||
// Do something with request error
|
||||
|
@ -51,6 +51,7 @@ const formState = ref({
|
||||
system_value: "",
|
||||
system_parent_id: 0,
|
||||
file: [],
|
||||
device_image: "",
|
||||
});
|
||||
|
||||
|
||||
@ -68,6 +69,7 @@ const openModal = (item) => {
|
||||
}
|
||||
: {};
|
||||
formState.value.file = [subFile];
|
||||
formState.value.device_image = item.device_image;
|
||||
}
|
||||
} else {
|
||||
formState.value = {
|
||||
@ -76,6 +78,7 @@ const openModal = (item) => {
|
||||
system_value: "",
|
||||
system_parent_id: 0,
|
||||
file: [],
|
||||
device_image: "",
|
||||
};
|
||||
}
|
||||
asset_add_sub_item.showModal();
|
||||
|
@ -20,6 +20,7 @@ const mainSystem = ref([]);
|
||||
const updateFileList = (files) => {
|
||||
console.log("file", files);
|
||||
props.formState.file = files;
|
||||
props.formState.device_image = files[0]?.name;
|
||||
};
|
||||
|
||||
const getMainSystems = async () => {
|
||||
@ -44,6 +45,7 @@ const onCancel = () => {
|
||||
system_value: "",
|
||||
system_parent_id: 0,
|
||||
file: [],
|
||||
device_image: "",
|
||||
};
|
||||
asset_add_sub_item.close();
|
||||
updateFileList([]);
|
||||
@ -71,16 +73,15 @@ const onOk = async () => {
|
||||
|
||||
if (props.formState.file[0]) {
|
||||
formData.append("file", props.formState.file[0]);
|
||||
}
|
||||
if (props.formState.Device_image) {
|
||||
formData.append("Device_image", props.formState.Device_image);
|
||||
formData.append("device_image", props.formState.device_image);
|
||||
}
|
||||
|
||||
// const res = await postAssetSubList(formData);
|
||||
// if (res.isSuccess) {
|
||||
// props.getData(parseInt(searchParams.value.mainSys_id));
|
||||
// onCancel();
|
||||
// }
|
||||
const res = await postAssetSubList(formData);
|
||||
if (res.isSuccess) {
|
||||
props.getData(parseInt(searchParams.value.mainSys_id));
|
||||
store.getSubMonitorPage(store.selectedBuilding.building_guid);
|
||||
onCancel();
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
@ -101,7 +102,7 @@ const onOk = async () => {
|
||||
>
|
||||
<template #modalContent>
|
||||
<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 #bottomLeft
|
||||
><span class="text-error text-base">
|
||||
|
@ -2,7 +2,7 @@
|
||||
import AlertSearch from "./AlertSearch.vue";
|
||||
import AlertTable from "./AlertTable.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 { getAlertLog } from "@/apis/alert";
|
||||
import {
|
||||
@ -15,7 +15,7 @@ import useBuildingStore from "@/stores/useBuildingStore";
|
||||
|
||||
const { searchParams } = useSearchParam();
|
||||
const store = useBuildingStore();
|
||||
|
||||
const hasSearched = ref(false);
|
||||
const tableLoading = ref(false);
|
||||
const dataSource = ref([]);
|
||||
const model_data = ref({
|
||||
@ -82,7 +82,7 @@ const openModal = async (record) => {
|
||||
updateEditRecord({
|
||||
...res.data,
|
||||
uuid: res.data.error_code,
|
||||
device_number: record.device_number
|
||||
device_number: record.device_number,
|
||||
});
|
||||
} else {
|
||||
updateEditRecord({
|
||||
@ -98,11 +98,31 @@ const openModal = async (record) => {
|
||||
|
||||
onMounted(() => {
|
||||
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_table", {
|
||||
openModal,
|
||||
|
@ -26,10 +26,6 @@ onMounted(() => {
|
||||
initializeItems();
|
||||
});
|
||||
|
||||
watch(locale, () => {
|
||||
initializeItems();
|
||||
});
|
||||
|
||||
// 監聽按鈕變化
|
||||
watch(
|
||||
selectedBtn,
|
||||
|
@ -12,7 +12,7 @@ const changeCheckedItem = () => {
|
||||
if (checkedItem.value.length === store.subSys.length) {
|
||||
changeParams({
|
||||
...searchParams.value,
|
||||
device_name_tag: [],
|
||||
device_name_tag: [store.subSys[0]?.sub_system_tag],
|
||||
});
|
||||
} else {
|
||||
changeParams({
|
||||
@ -50,11 +50,12 @@ const checkedItem = computed(() =>
|
||||
: []
|
||||
);
|
||||
|
||||
// 初始化想要全選
|
||||
watch(searchParams, (newValue) => {
|
||||
if (!newValue.device_name_tag) {
|
||||
changeParams({
|
||||
...newValue,
|
||||
device_name_tag: [store.subSys[0]?.sub_system_tag],
|
||||
device_name_tag: store.subSys.map(({ sub_system_tag }) => sub_system_tag),
|
||||
});
|
||||
}
|
||||
});
|
||||
|
@ -2,7 +2,10 @@
|
||||
import DashboardFloorBar from "./components/DashboardFloorBar.vue";
|
||||
import DashboardEffectScatter from "./components/DashboardEffectScatter.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 DashboardHumidity from "./components/DashboardHumidity.vue";
|
||||
import DashboardRefrigTemp from "./components/DashboardRefrigTemp.vue";
|
||||
import DashboardIndoorTemp from "./components/DashboardIndoorTemp.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"
|
||||
>
|
||||
<div>
|
||||
<DashboardProduct />
|
||||
</div>
|
||||
<div class="mt-6">
|
||||
<DashboardProductComplete />
|
||||
</div>
|
||||
<div class="mt-6">
|
||||
<DashboardTemp />
|
||||
</div>
|
||||
<div class="mt-10">
|
||||
<DashboardAlert />
|
||||
<DashboardHumidity />
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
@ -135,6 +144,9 @@ onUnmounted(() => {
|
||||
<div class="mt-10">
|
||||
<DashboardEmission />
|
||||
</div>
|
||||
<div class="mt-10">
|
||||
<DashboardAlert />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -9,12 +9,16 @@ let intervalId = null; // 用來儲存 setInterval 的 ID
|
||||
|
||||
const getAlarmData = async (building_guid) => {
|
||||
const res = await getAlertLogList(building_guid);
|
||||
dataSource.value = (res.data || []);
|
||||
dataSource.value = res.data || [];
|
||||
};
|
||||
|
||||
watch(
|
||||
() => store.selectedBuilding,
|
||||
(newBuilding) => {
|
||||
if (intervalId) {
|
||||
clearInterval(intervalId);
|
||||
intervalId = null;
|
||||
}
|
||||
if (newBuilding) {
|
||||
getAlarmData(newBuilding.building_guid);
|
||||
|
||||
@ -38,7 +42,7 @@ onUnmounted(() => {
|
||||
<h3 class="text-info text-xl text-center">
|
||||
{{ $t("dashboard.alerts_data") }} Top 5
|
||||
</h3>
|
||||
<div class="overflow-x-auto">
|
||||
<div class="overflow-x-auto min-h-[300px]">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr class="border-b-2 border-info text-base">
|
||||
|
@ -41,7 +41,7 @@ const defaultOption = (map, data = []) => {
|
||||
},
|
||||
map,
|
||||
roam: true, // 允許縮放和平移
|
||||
layoutSize: window.innerWidth <= 768 ? "110%" : "100%",
|
||||
layoutSize: window.innerWidth <= 768 ? "110%" : "80%",
|
||||
layoutCenter: ["50%", "50%"],
|
||||
scaleLimit: { min: 1, max: 2 },
|
||||
},
|
||||
|
@ -72,7 +72,7 @@ const onCancel = () => {
|
||||
|
||||
<template>
|
||||
<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"
|
||||
>
|
||||
{{ $t("button.edit") }}
|
||||
|
@ -67,7 +67,7 @@ const closeModal = () => {
|
||||
</script>
|
||||
|
||||
<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") }}
|
||||
</button>
|
||||
<Modal
|
||||
|
213
src/views/dashboard/components/DashboardHumidity.vue
Normal file
213
src/views/dashboard/components/DashboardHumidity.vue
Normal 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>
|
153
src/views/dashboard/components/DashboardProduct.vue
Normal file
153
src/views/dashboard/components/DashboardProduct.vue
Normal 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>
|
95
src/views/dashboard/components/DashboardProductComplete.vue
Normal file
95
src/views/dashboard/components/DashboardProductComplete.vue
Normal 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>
|
104
src/views/dashboard/components/DashboardProductCompleteModal.vue
Normal file
104
src/views/dashboard/components/DashboardProductCompleteModal.vue
Normal 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>
|
@ -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>
|
||||
|
@ -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>
|
@ -77,32 +77,40 @@ const getData = async (tempOption) => {
|
||||
|
||||
// 監聽建築物選擇變化
|
||||
watch(
|
||||
() => buildingStore.selectedBuilding?.building_guid,
|
||||
(newBuildingGuid) => {
|
||||
if (newBuildingGuid) {
|
||||
getData(1);
|
||||
timeoutTimer.value = setInterval(() => {
|
||||
getData(1);
|
||||
}, 60 * 1000);
|
||||
() => 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);
|
||||
}
|
||||
}
|
||||
|
||||
setItems([
|
||||
{
|
||||
title: "室內溫度",
|
||||
key: 1,
|
||||
active: true,
|
||||
},
|
||||
{
|
||||
title: "冷藏溫度",
|
||||
key: 2,
|
||||
active: false,
|
||||
},
|
||||
]);
|
||||
},
|
||||
{
|
||||
immediate: true,
|
||||
@ -115,7 +123,7 @@ watch(
|
||||
if (timeoutTimer.value) {
|
||||
clearInterval(timeoutTimer.value);
|
||||
}
|
||||
|
||||
|
||||
if (newValue?.key) {
|
||||
getData(newValue.key);
|
||||
// 重新設置定時器
|
||||
@ -125,6 +133,7 @@ watch(
|
||||
}
|
||||
},
|
||||
{
|
||||
immediate: true,
|
||||
deep: true,
|
||||
}
|
||||
);
|
||||
@ -135,18 +144,20 @@ watch(
|
||||
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);
|
||||
|
||||
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 }) => time), // 使用 time
|
||||
data: firstItem.data.map(({ time }) => dayjs(time).format("HH:mm:ss")),
|
||||
},
|
||||
yAxis: {
|
||||
min: Math.floor(minValue),
|
||||
@ -178,23 +189,20 @@ onUnmounted(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col justify-center mb-3">
|
||||
<h3 class="text-info font-bold text-xl text-center">溫度趨勢</h3>
|
||||
<div className="mt-2 w-full flex justify-center relative">
|
||||
<ButtonConnectedGroup
|
||||
:items="items"
|
||||
:onclick="
|
||||
(e, item) => {
|
||||
changeActiveBtn(item);
|
||||
}
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
<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-[350px] max-h-fit"
|
||||
class="min-h-[260px] max-h-fit"
|
||||
:option="defaultChartOption"
|
||||
ref="other_real_temp_chart"
|
||||
/>
|
||||
|
@ -24,29 +24,34 @@ watch(
|
||||
item.costPeak + item.costHalfPeak + item.costOffPeak + item.costBase;
|
||||
return sum + monthlyBill;
|
||||
}, 0)
|
||||
.toFixed(2);
|
||||
.toFixed(2)
|
||||
.replace(/\B(?=(\d{3})+(?!\d))/g, ",");
|
||||
totalElecDegree.value = newData
|
||||
.reduce((sum, item) => {
|
||||
const monthlyBill =
|
||||
item.degreePeak + item.degreeHalfPeak + item.degreeOffPeak;
|
||||
return sum + monthlyBill;
|
||||
}, 0)
|
||||
.toFixed(2);
|
||||
|
||||
.toFixed(2)
|
||||
.replace(/\B(?=(\d{3})+(?!\d))/g, ",");
|
||||
|
||||
// 計算區間電費與用電度數,取最新一筆數據
|
||||
const latestData = newData[newData.length - 1];
|
||||
IntervalElecBills.value = (
|
||||
latestData.costPeak +
|
||||
latestData.costHalfPeak +
|
||||
latestData.costOffPeak +
|
||||
latestData.costOffPeak +
|
||||
latestData.costBase
|
||||
).toFixed(2);
|
||||
)
|
||||
.toFixed(2)
|
||||
.replace(/\B(?=(\d{3})+(?!\d))/g, ",");
|
||||
IntervalElecDegree.value = (
|
||||
latestData.degreePeak +
|
||||
latestData.degreeHalfPeak +
|
||||
latestData.degreeOffPeak
|
||||
).toFixed(2);
|
||||
)
|
||||
.toFixed(2)
|
||||
.replace(/\B(?=(\d{3})+(?!\d))/g, ",");
|
||||
const monthDays = latestData.time ? daysInMonth(latestData.time) : 0;
|
||||
IntervalDate.value = latestData.time
|
||||
? `${latestData.time}-01~${latestData.time}-${monthDays}`
|
||||
|
@ -8,28 +8,28 @@ import { ref, provide } from "vue";
|
||||
const tableData = ref([]);
|
||||
const loading = ref(false);
|
||||
const updateTableData = (data) => {
|
||||
tableData.value = data
|
||||
? data.items.map((d) => ({
|
||||
...d,
|
||||
key: `${d.device_number}_${d.points}_${dayjs(d.timestamp).format("x")}`,
|
||||
}))
|
||||
: [];
|
||||
tableData.value = data ? data : [];
|
||||
};
|
||||
|
||||
const updateLoading = () => {
|
||||
loading.value = !loading.value;
|
||||
};
|
||||
|
||||
provide("history_table_data", { tableData, updateTableData, loading, updateLoading });
|
||||
provide("history_table_data", {
|
||||
tableData,
|
||||
updateTableData,
|
||||
loading,
|
||||
updateLoading,
|
||||
});
|
||||
</script>
|
||||
|
||||
<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="col-span-10 lg:col-span-2 ">
|
||||
<div class="col-span-10 lg:col-span-2">
|
||||
<HistorySidebar />
|
||||
</div>
|
||||
<div class="col-span-10 lg:col-span-8 ">
|
||||
<div class="col-span-10 lg:col-span-8">
|
||||
<HistorySearch />
|
||||
<HistoryTable />
|
||||
</div>
|
||||
|
@ -8,7 +8,6 @@ const { searchParams } = useSearchParam();
|
||||
|
||||
const props = defineProps({
|
||||
form: Object,
|
||||
selectedType: String,
|
||||
});
|
||||
|
||||
const { updateTableData, updateLoading } = inject("history_table_data");
|
||||
@ -53,6 +52,7 @@ const submit = async (e, type = "") => {
|
||||
updateLoading();
|
||||
const res = await getHistoryData({
|
||||
type: searchParams.value.selectedType,
|
||||
table_type:1,
|
||||
...params,
|
||||
...searchParams.value,
|
||||
});
|
||||
@ -118,13 +118,18 @@ watch(
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Toast
|
||||
<Toast
|
||||
:content="isToastOpen.content"
|
||||
:open="isToastOpen.open"
|
||||
status="info"
|
||||
: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>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
|
164
src/views/history/components/HistoryDataCahrt.vue
Normal file
164
src/views/history/components/HistoryDataCahrt.vue
Normal 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>
|
@ -15,7 +15,11 @@ const searchTerm = ref(""); //搜尋文字
|
||||
const activeSearchTerm = ref("");
|
||||
|
||||
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({
|
||||
sub_system_tag: sub_system_tag,
|
||||
department_id: deptArray,
|
||||
@ -110,7 +114,7 @@ const checkedBuilding = computed(() => {
|
||||
|
||||
if (
|
||||
selectedDeviceNumber.value?.filter(
|
||||
(d) => d.split("_")[1] === building_tag
|
||||
(d) => d && d.split("_")[1] === building_tag // 加上 d && 判斷
|
||||
).length === allDevices.length
|
||||
) {
|
||||
selected.push(building_tag);
|
||||
|
@ -1,14 +1,20 @@
|
||||
<script setup>
|
||||
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";
|
||||
const { t } = useI18n();
|
||||
const { tableData, loading } = inject("history_table_data");
|
||||
const processedTableData = ref([]);
|
||||
|
||||
const columns = computed(() => [
|
||||
{
|
||||
title: t("history.building_name"),
|
||||
key: "building_name",
|
||||
sort: true,
|
||||
title: t("assetManagement.department"),
|
||||
key: "department_name",
|
||||
},
|
||||
{
|
||||
title: t("energy.floor"),
|
||||
key: "floor_name",
|
||||
},
|
||||
{
|
||||
title: t("history.device_name"),
|
||||
@ -18,7 +24,7 @@ const columns = computed(() => [
|
||||
sort: true,
|
||||
},
|
||||
{
|
||||
title: t("history.category"),
|
||||
title: t("assetManagement.point"),
|
||||
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>
|
||||
|
||||
<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>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
|
Loading…
Reference in New Issue
Block a user