預設繁體中文 | 拿掉右上告警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_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`;
|
@ -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,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
@ -121,9 +121,9 @@ watch(locale, () => {
|
|||||||
<li>
|
<li>
|
||||||
<NavbarLang />
|
<NavbarLang />
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<!-- <li>
|
||||||
<AlarmDrawer />
|
<AlarmDrawer />
|
||||||
</li>
|
</li> -->
|
||||||
<li>
|
<li>
|
||||||
<NavbarUser />
|
<NavbarUser />
|
||||||
</li>
|
</li>
|
||||||
|
@ -6,7 +6,7 @@ const store = useBuildingStore();
|
|||||||
|
|
||||||
const selectBuilding = (bui) => {
|
const selectBuilding = (bui) => {
|
||||||
store.selectedBuilding = bui; // 改變 selectedBuilding,watch 會自動更新資料
|
store.selectedBuilding = bui; // 改變 selectedBuilding,watch 會自動更新資料
|
||||||
localStorage.setItem("CviBuilding", JSON.stringify(bui));
|
localStorage.setItem("EmpowerBuilding", JSON.stringify(bui));
|
||||||
};
|
};
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
@ -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>
|
||||||
|
|
||||||
|
@ -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>
|
||||||
|
@ -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);
|
||||||
|
@ -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" });
|
||||||
}
|
}
|
||||||
|
@ -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,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
@ -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
|
||||||
|
@ -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();
|
||||||
|
@ -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">
|
||||||
|
@ -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,
|
||||||
|
@ -26,10 +26,6 @@ onMounted(() => {
|
|||||||
initializeItems();
|
initializeItems();
|
||||||
});
|
});
|
||||||
|
|
||||||
watch(locale, () => {
|
|
||||||
initializeItems();
|
|
||||||
});
|
|
||||||
|
|
||||||
// 監聽按鈕變化
|
// 監聽按鈕變化
|
||||||
watch(
|
watch(
|
||||||
selectedBtn,
|
selectedBtn,
|
||||||
|
@ -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),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -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>
|
||||||
|
@ -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">
|
||||||
|
@ -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 },
|
||||||
},
|
},
|
||||||
|
@ -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") }}
|
||||||
|
@ -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
|
||||||
|
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(
|
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"
|
||||||
/>
|
/>
|
||||||
|
@ -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}`
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
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 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);
|
||||||
|
@ -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>
|
||||||
|
Loading…
Reference in New Issue
Block a user