登入帳號紀錄cookie | table的過濾篩選可以輸入關鍵字 | 能源管理: 新增能源分析、能號報表 | 帳號管理:過濾webUser、admin | 告警設定: 新增警示時間 | 設備管理: iot欄位預設、tag_name只有在webUser時顯示、系統類別和設備類別的更新時如果沒有subSys_id時表格清空新增設備按鈕也不能按、樓層2d圖更新bug修正 | 系統監控: 系統小卡的desktop要根據ori_device_name分類

This commit is contained in:
koko 2025-01-24 10:43:50 +08:00
parent 9e5ff1544c
commit 8e2b5e1e2c
54 changed files with 2294 additions and 335 deletions

View File

@ -14,3 +14,7 @@ export const GET_OUTLIERS_LIST_API = `api/Alarm/GetAlarmSetting`;
export const GET_OUTLIERS_DEVLIST_API = `api/Alarm/GetDevList`; // 取得設備
export const GET_OUTLIERS_POINTS_API = `api/Alarm/GetAlarmPoints`; // 取得點位
export const POST_OUTLIERS_SETTING_API = `api/Alarm/SaveAlarmSetting`; // 新增與修改
export const GET_ALERT_SCHEDULE_LIST_API = `api/Alarm/GetAlarmSchedule`;
export const POST_ALERT_SCHEDULE = `api/Alarm/SaveAlarmSchedule`;
export const DELETE_ALERT_SCHEDULE = `api/Alarm/DeleteAlarmSchedule`;

View File

@ -12,6 +12,9 @@ import {
POST_ALERT_MEMBER,
DELETE_ALERT_MEMBER,
GET_NOTICE_LIST_API,
GET_ALERT_SCHEDULE_LIST_API,
POST_ALERT_SCHEDULE,
DELETE_ALERT_SCHEDULE,
} from "./api";
import instance from "@/util/request";
import apihandler from "@/util/apihandler";
@ -156,3 +159,34 @@ export const postOutliersSetting = async (data) => {
code: res.code,
});
};
export const getAlarmScheduleList = async () => {
const res = await instance.post(GET_ALERT_SCHEDULE_LIST_API, {});
return apihandler(res.code, res.data, {
msg: res.msg,
code: res.code,
});
};
export const postAlertSchedule = async (data) => {
const res = await instance.post(POST_ALERT_SCHEDULE, data);
return apihandler(res.code, res.data, {
msg: res.msg,
code: res.code,
});
};
export const deleteAlarmSchedule = async (id) => {
try {
const res = await instance.post(DELETE_ALERT_SCHEDULE, { id });
return {
isSuccess: res.code === "0000",
msg: res.msg || "刪除成功",
};
} catch (error) {
console.error("API request failed", error);
return { isSuccess: false, msg: "API request failed" };
}
};

View File

@ -24,3 +24,5 @@ export const GET_ASSET_IOT_SCHEMA_API = `/AssetManage/GetResponseSchema`;
export const GET_ASSET_DEPARTMENT_API = `/AssetManage/GetDepartment`;
export const POST_ASSET_DEPARTMENT_API = `/AssetManage/SaveDepartment`;
export const DELETE_ASSET_DEPARTMENT_API = `/AssetManage/DeleteDepartment`;
export const GET_ASSET_ELECTYPE_API = `/AssetManage/GetElecType`;

View File

@ -18,6 +18,7 @@ import {
GET_ASSET_DEPARTMENT_API,
POST_ASSET_DEPARTMENT_API,
DELETE_ASSET_DEPARTMENT_API,
GET_ASSET_ELECTYPE_API,
} from "./api";
import instance from "@/util/request";
import apihandler from "@/util/apihandler";
@ -203,8 +204,8 @@ export const getAssetSubPoint = async (sub_system_tag) => {
});
};
export const getIOTSchema = async (sub_system_tag, points) => {
const res = await instance.post(GET_ASSET_IOT_SCHEMA_API, {});
export const getIOTSchema = async (variable_id) => {
const res = await instance.post(GET_ASSET_IOT_SCHEMA_API, {variable_id});
return apihandler(res.code, res.data, {
msg: res.msg,
@ -241,3 +242,12 @@ export const deleteDepartmentItem = async (id) => {
code: res.code,
});
};
export const getElecTypeList = async () => {
const res = await instance.post(GET_ASSET_ELECTYPE_API, {});
return apihandler(res.code, res.data, {
msg: res.msg,
code: res.code,
});
};

View File

@ -1,3 +1,7 @@
export const GET_REALTIME_DIST_API = `/api/Energe/GetRealTimeDistribution`;
export const GET_ELECUSE_DAY_API = `/api/Energe/GetElecUseDay`;
export const GET_TAI_POWER_API = `/api/Energe/GetTaipower`;
export const GET_SIDEBAR_API = `/api/Energe/GetEnergySideBar`;
export const GET_SEARCH_API = `/api/Energe/GetFilter`;

View File

@ -2,6 +2,8 @@ import {
GET_REALTIME_DIST_API,
GET_ELECUSE_DAY_API,
GET_TAI_POWER_API,
GET_SIDEBAR_API,
GET_SEARCH_API,
} from "./api";
import instance from "@/util/request";
import apihandler from "@/util/apihandler";
@ -32,3 +34,21 @@ export const getTaipower = async () => {
code: res.code,
});
};
export const getEnergySideBar = async () => {
const res = await instance.post(GET_SIDEBAR_API);
return apihandler(res.code, res.data, {
msg: res.msg,
code: res.code,
});
};
export const getEnergySearch = async (type) => {
const res = await instance.post(GET_SEARCH_API, { type });
return apihandler(res.code, res.data, {
msg: res.msg,
code: res.code,
});
};

View File

@ -12,9 +12,15 @@ import instance, { fileInstance } from "@/util/request";
import apihandler from "@/util/apiHandler";
import downloadExcel from "@/util/downloadExcel";
export const getHistorySideBar = async (sub_system_tag) => {
export const getHistorySideBar = async ({
sub_system_tag,
department_id,
elec_type_id,
}) => {
const res = await instance.post(GET_HISTORY_SIDEBAR_API, {
sub_system_tag,
department_id,
elec_type_id,
});
return apihandler(res.code, res.data, {
@ -42,6 +48,7 @@ export const getHistoryData = async ({
End_time,
Device_list,
Points,
table_type,
}) => {
/*
{
@ -62,6 +69,7 @@ export const getHistoryData = async ({
Device_list: Array.isArray(Device_list) ? Device_list : [Device_list],
Points: Array.isArray(Points) ? Points : [Points],
Type: parseInt(Type),
table_type: parseInt(table_type),
});
return apihandler(res.code, res.data, {

View File

@ -13,6 +13,10 @@ export async function Login({ account, password }) {
document.cookie = `JWT-Authorization=${res.data.token}; Max-Age=${
24 * 60 * 60 * 1000
}`;
// 設定 user_name Cookie
document.cookie = `user_name=${res.data.user_name}; Max-Age=${
24 * 60 * 60 * 1000
}`;
}
return apihandler(res.code, res.data, {

View File

@ -118,6 +118,6 @@ const curWidth = computed(() => {
</style>
<style scoped>
.line {
@apply mr-3 relative z-20 after:absolute after:top-1/2 after:-right-3 after:w-3 after:h-[1px] after:bg-info after:z-10 last:after:h-0;
@apply mr-3 relative z-30 after:absolute after:top-1/2 after:-right-3 after:w-3 after:h-[1px] after:bg-info after:z-10 last:after:h-0;
}
</style>

View File

@ -51,7 +51,7 @@ watch(
);
const updateDataSource = (data) => {
console.log("update", data);
// console.log("update", data);
currentDataSource.value = data;
};
provide("current_table_data", {
@ -62,6 +62,7 @@ const sortRule = ref({});
const filterColumn = ref({});
const filterItems = ref({});
const selectedFilterItem = ref({});
const searchQuery = ref("");
const toggleFilterModal = (key) => {
let newFilter = Object.assign(filterColumn.value);
@ -136,7 +137,10 @@ const form = ref(null);
const onFilter = (key, reset = false) => {
const formData = new FormData(form.value);
reset && formData.delete(key);
if (reset) {
formData.delete(key);
searchQuery.value = "";
}
for (let [name, value] of formData) {
console.log(name, value);
}
@ -232,13 +236,28 @@ watch(
"
@click="() => toggleFilterModal(column.key)"
/>
<div
class="absolute top-full -left-1/2 z-50"
v-if="filterColumn[column.key]"
>
<div class="fixed z-50" v-if="filterColumn[column.key]">
<div class="card min-w-max bg-body shadow-xl px-10 py-5">
<label
class="input input-bordered bg-transparent rounded-lg flex items-center px-2 mb-4 border-success focus-within:border-success"
>
<font-awesome-icon
:icon="['fas', 'search']"
class="w-6 h-6 mr-2 text-success"
/>
<input
type="text"
:placeholder="t('operation.enter_text')"
class="text-white bg-transparent w-full"
v-model="searchQuery"
/>
</label>
<div class="max-h-72 overflow-x-auto px-2">
<Checkbox
v-for="item in filterItems[column.key]"
v-for="item in filterItems[column.key].filter(
(item) =>
item.name && item.name.includes(searchQuery)
)"
:title="item.name"
:value="item.name"
:key="item.name"
@ -248,6 +267,7 @@ watch(
"
className="justify-start"
/>
</div>
<div class="card-actions mt-4 justify-end">
<input
type="reset"

View File

@ -2,6 +2,7 @@
import { onMounted, ref, watch, computed } from "vue";
import { AUTHPAGES } from "@/constant";
import { getAuth, getAllSysSidebar } from "@/apis/building";
import { getEnergySideBar } from "@/apis/energy";
import useBuildingStore from "@/stores/useBuildingStore";
import useUserInfoStore from "@/stores/useUserInfoStore";
import { useI18n } from "vue-i18n";
@ -13,6 +14,8 @@ const store = useUserInfoStore();
const buildingStore = useBuildingStore();
const route = useRoute();
const openKeys = ref([]); //
const menu_array = ref([]);
const currentAuthCode = ref("");
const iniFroList = async () => {
const res = await getAuth(locale.value);
@ -37,9 +40,19 @@ const open = ref(false);
const getSubMonitorPage = async (building) => {
const res = await getAllSysSidebar();
buildingStore.mainSubSys = res.data.history_Main_Systems;
menu_array.value = res.data.history_Main_Systems;
};
const showDrawer = () => {
const getSubEnergyPage = async () => {
const res = await getEnergySideBar();
menu_array.value = res.data;
};
const showDrawer = (authCode) => {
if (authCode === "PF1") {
getSubMonitorPage();
} else if (authCode === "PF2") {
getSubEnergyPage();
}
currentAuthCode.value = authCode;
open.value = true;
};
const onClose = () => {
@ -89,14 +102,19 @@ onMounted(() => {
<li
v-for="page in authPages"
class="flex flex-col items-center justify-center"
:key="page.authCode"
>
<a
v-if="page.authCode === 'PF1'"
@click="showDrawer"
v-if="page.authCode === 'PF1' || page.authCode === 'PF2'"
@click="showDrawer(page.authCode)"
:class="
twMerge(
'flex lg:flex-col justify-center items-center btn-group text-white cursor-pointer',
route.fullPath.includes('/system')
page.authCode === 'PF1' && route.fullPath.includes('/system')
? 'router-link-active router-link-exact-active'
: '',
page.authCode === 'PF2' &&
route.fullPath.includes('/energyManagement')
? 'router-link-active router-link-exact-active'
: ''
)
@ -142,22 +160,26 @@ onMounted(() => {
@openChange="handleOpenChange"
>
<a-sub-menu
v-for="main in buildingStore.mainSubSys"
v-for="main in menu_array"
:key="main.main_system_tag"
:title="main.full_name"
v-if="menu_array.length > 0 && open"
>
<a-menu-item
v-for="sub in main.history_Sub_systems"
:key="sub.sub_system_tag"
v-for="sub in currentAuthCode === 'PF1'
? main.history_Sub_systems
: main.sub"
:key="sub.sub_system_tag+`_`+sub.type"
@click="() => onClose()"
>
<router-link
:to="{
name: 'sub_system',
name:
currentAuthCode === 'PF2' ? 'energyManagement' : 'sub_system',
params: {
main_system_id: main.main_system_tag,
sub_system_id: sub.sub_system_tag,
floor_id: 'main',
...(currentAuthCode === 'PF2' ? { type: sub.type } : { floor_id: 'main' }),
},
}"
>

View File

@ -82,7 +82,23 @@
"total_elec_cost": "总电费",
"carbon_equivalent": "碳排当量",
"edit_carbon_emission": "编辑碳排放系数",
"carbon_emission_coefficient": "碳排放系数"
"carbon_emission_coefficient": "碳排放系数",
"electricity_classification": "用电分类",
"electricity_price": "电费每度单价",
"floor": "楼层",
"maximum": "最大值",
"maximum_time": "最大值时间",
"minimum": "最小值",
"minimum_time": "最小值时间",
"average_value": "平均值",
"start_value": "起始值(kWh)",
"end_value": "截止值(kWh)",
"difference": "差值(kWh)",
"power_consumption": "用电量(kWh)",
"ranking": "排名",
"subtotal": "小计",
"unit_price": "单价",
"total_amount": "金额总计"
},
"alarm": {
"title": "显示警告",
@ -152,7 +168,16 @@
"choose": "选择",
"day_time": "星期/时间",
"click_time_period": "请用滑鼠点击时间段",
"clear": "清空"
"clear": "清空",
"sunday": "星期日",
"monday": "星期一",
"tuesday": "星期二",
"wednesday": "星期三",
"thursday": "星期四",
"friday": "星期五",
"saturday": "星期六",
"schedule_name": "时段名称",
"schedule_content": "时段内容"
},
"operation": {
"title": "运维管理",

View File

@ -82,7 +82,23 @@
"total_elec_cost": "總電費",
"carbon_equivalent": "碳排當量",
"edit_carbon_emission": "編輯碳排放係數",
"carbon_emission_coefficient":"碳排放係數"
"carbon_emission_coefficient": "碳排放係數",
"electricity_classification": "用電分類",
"electricity_price": "電費每度單價",
"floor": "樓層",
"maximum": "最大值",
"maximum_time": "最大值時間",
"minimum": "最小值",
"minimum_time": "最小值時間",
"average_value": "平均值",
"start_value": "起始值(kWh)",
"end_value": "截止值(kWh)",
"difference": "差值(kWh)",
"power_consumption": "用電量(kWh)",
"ranking": "排名",
"subtotal": "小計",
"unit_price": "單價",
"total_amount": "金額總計"
},
"alarm": {
"title": "顯示警告",
@ -152,7 +168,16 @@
"choose": "選擇",
"day_time": "星期/時間",
"click_time_period": "請用滑鼠點擊時間段",
"clear":"清空"
"clear": "清空",
"sunday": "星期日",
"monday": "星期一",
"tuesday": "星期二",
"wednesday": "星期三",
"thursday": "星期四",
"friday": "星期五",
"saturday": "星期六",
"schedule_name": "時段名稱",
"schedule_content": "時段內容"
},
"operation": {
"title": "運維管理",

View File

@ -82,7 +82,23 @@
"total_elec_cost": "Total Elec. Cost",
"carbon_equivalent": "Carbon Equivalent",
"edit_carbon_emission": "Edit carbon emission coefficient",
"carbon_emission_coefficient": "Carbon emission coefficient"
"carbon_emission_coefficient": "Carbon emission coefficient",
"electricity_classification": "Electricity Classification",
"electricity_price": "Electricity charge per unit price",
"floor": "Floor",
"maximum": "Maximum",
"maximum_time": "Maximum time",
"minimum": "Minimum value",
"minimum_time": "Minimum time",
"average_value": "Average value",
"start_value": "Start value (kWh)",
"end_value": "End value (kWh)",
"difference": "Difference (kWh)",
"power_consumption": "Power consumption (kWh)",
"ranking": "Ranking",
"subtotal": "Subtotal",
"unit_price": "Unit price",
"total_amount": "Total amount"
},
"alarm": {
"title": "Warning",
@ -152,7 +168,16 @@
"choose": "Choose",
"day_time": "Week/Time",
"click_time_period": "Please click the time period with your mouse",
"clear": "Clear"
"clear": "Clear",
"sunday": "Sunday",
"monday": "Monday",
"tuesday": "Tuesday",
"wednesday": "Wednesday",
"thursday": "Thursday",
"friday": "Friday",
"saturday": "Saturday",
"schedule_name": "Time period name",
"schedule_content": "Time period content"
},
"operation": {
"title": "Operation And Maintenance Management",

View File

@ -80,7 +80,7 @@ const router = createRouter({
component: ProductSetting,
},
{
path: "/energyManagement",
path: "/energyManagement/:main_system_id/:sub_system_id/:type",
name: "energyManagement",
component: EnergyManagement,
},
@ -99,10 +99,13 @@ router.beforeEach(async (to, from, next) => {
const authRequired = !publicPages.includes(to.path);
const auth = useUserInfoStore();
const token = useGetCookie("JWT-Authorization");
const user_name = useGetCookie("user_name");
if (to.path === "/logout") {
document.cookie = "JWT-Authorization=";
document.cookie = "JWT-Authorization=; Max-Age=0";
document.cookie = "user_name=; Max-Age=0";
auth.user.token = "";
auth.user.user_name = "";
window.location.reload();
next({ path: "/login" });
}
@ -111,10 +114,13 @@ router.beforeEach(async (to, from, next) => {
auth.user.token = "";
next({ path: "/login" });
} else if (!authRequired) {
document.cookie = "JWT-Authorization=";
document.cookie = "JWT-Authorization=; Max-Age=0";
document.cookie = "user_name=; Max-Age=0";
auth.user.token = "";
auth.user.user_name = "";
} else {
auth.user.token = token;
auth.user.user_name = user_name;
}
next();
});

View File

@ -5,6 +5,7 @@ const useUserInfoStore = defineStore("userInfo", () => {
const user = ref({
token: "",
expires: 0,
user_name:"",
});
const auth_page = ref([]);

View File

@ -1,7 +1,43 @@
<script setup>
import { ref, provide, onMounted, watch } from "vue";
import AssetMainList from "./components/AssetMainList.vue";
import AssetSubList from "./components/AssetSubList.vue";
import AssetTable from "./components/AssetTable.vue";
import { getOperationCompanyList } from "@/apis/operation";
import { getIOTSchema } from "@/apis/asset";
import useSearchParam from "@/hooks/useSearchParam";
const { searchParams, changeParams } = useSearchParam();
const companyOptions = ref([]);
const iotSchemaOptions = ref([]);
const getCompany = async () => {
const res = await getOperationCompanyList();
companyOptions.value = res.data.map((d) => ({ ...d, key: d.id }));
};
const getIOTSchemaOptions = async (id) => {
const res = await getIOTSchema(Number(id));
iotSchemaOptions.value = res.data.map((d) => ({ ...d, key: d.id }));
};
onMounted(() => {
getCompany();
});
watch(
() => searchParams.value.subSys_id,
(newValue) => {
if (newValue) {
getIOTSchemaOptions(newValue);
}
},
{
immediate: true,
}
);
provide("asset_modal_options", {
companyOptions,
iotSchemaOptions,
});
</script>
<template>

View File

@ -66,7 +66,6 @@ onMounted(() => {
watch(selectedBtn, (newValue) => {
changeParams({
...searchParams.value,
mainSys_id: newValue.key,
subSys_id: null,
});

View File

@ -39,7 +39,6 @@ const getAssetData = async () => {
}
totalCoordinates.value[floorGuid].push(d.device_coordinate);
});
tableData.value = res.data.map((d) => ({
...d,
key: d.id,
@ -122,6 +121,8 @@ watch(
(newValue) => {
if (newValue.value?.subSys_id) {
getAssetData();
}else{
tableData.value=[];
}
},
{

View File

@ -102,7 +102,7 @@ const closeModal = () => {
</script>
<template>
<button class="btn btn-sm btn-add mr-3" @click.stop.prevent="openModal">
<button class="btn btn-sm btn-add mr-3" @click.stop.prevent="openModal" :disabled="!searchParams.subSys_id">
<font-awesome-icon :icon="['fas', 'plus']" />{{ $t("button.add") }}
</button>
<Modal

View File

@ -6,19 +6,17 @@ import AssetTableModalLeftInfoIoT from "./AssetTableModalLeftInfoIoT.vue";
import AssetTableModalLeftInfoDept from "./AssetTableModalLeftInfoDept.vue";
import AssetTableModalLeftInfoGraph from "./AssetTableModalLeftInfoGraph.vue";
import AssetTableModalLeftInfoMQTT from "./AssetTableModalLeftInfoMQTT.vue";
import { getOperationCompanyList } from "@/apis/operation";
import { getIOTSchema } from "@/apis/asset";
import useSearchParam from "@/hooks/useSearchParam";
import useUserInfoStore from "@/stores/useUserInfoStore";
import dayjs from "dayjs";
import { useI18n } from "vue-i18n";
const { t } = useI18n();
const FILE_BASEURL = import.meta.env.VITE_FILE_API_BASEURL;
const { searchParams, changeParams } = useSearchParam();
const { updateLeftFields, formErrorMsg, formState } = inject(
"asset_table_modal_form"
);
const { companyOptions, iotSchemaOptions } = inject("asset_modal_options");
const store = useUserInfoStore();
let schema = {
full_name: yup.string().nullable(true),
operate_text: yup.string().nullable(true),
@ -94,20 +92,15 @@ watch(formState, (newValue) => {
}
});
const companyOptions = ref([]);
const iotSchemaOptions = ref([]);
const getCompany = async () => {
const res = await getOperationCompanyList();
companyOptions.value = res.data.map((d) => ({ ...d, key: d.id }));
};
const getIOTSchemaOptions = async () => {
const res = await getIOTSchema();
iotSchemaOptions.value = res.data.map((d) => ({ ...d, key: d.id }));
};
onMounted(() => {
getCompany();
getIOTSchemaOptions();
});
watch(
() => iotSchemaOptions.value,
(newVal) => {
if (newVal && newVal.length > 0) {
formState.value.response_schema_id = newVal[0].id;
}
},
{ immediate: true }
);
</script>
<template>
@ -121,7 +114,12 @@ onMounted(() => {
></Input
>
<AssetTableModalLeftInfoDept />
<Input :value="formState" width="290" name="device_number">
<Input
:value="formState"
width="290"
name="device_number"
v-if="store.user.user_name == 'webUser'"
>
<template #topLeft
>Tag_Name ({{ $t("assetManagement.fill_text") }})</template
>

View File

@ -44,6 +44,7 @@ const modalColumns = computed(() => [
{
title: "tag",
key: "device_name",
filter: true
},
{
title: t("assetManagement.point"),

View File

@ -35,6 +35,7 @@ onBeforeMount(() => {
const asset_floor_chart = ref(null);
const currentFloor = ref(null);
const selectedOption = ref("add");
const parsedCoordinates = ref(null);
const defaultOption = (map, data = []) => {
//
@ -73,17 +74,21 @@ const defaultOption = (map, data = []) => {
watch(currentFloor, (newValue) => {
if (newValue?.floor_map_name) {
const coordinates = totalCoordinates.value[newValue.floor_guid] || [];
if (coordinates.length === 0 || coordinates.every(coord => coord === "")) return;
const parsedCoordinates = coordinates.map((coord) => {
return JSON.parse(coord);
});
const coordinates =
(totalCoordinates.value?.[newValue.floor_guid]?.filter(
(coord) => coord !== ""
) || []);
parsedCoordinates.value = coordinates.length > 0
? coordinates.map((coord) => JSON.parse(coord))
: [];
asset_floor_chart.value.updateSvg(
{
full_name: newValue?.floor_map_name,
path: `${FILE_BASEURL}/${newValue?.floor_map_url}.svg`,
full_name: newValue.floor_map_name,
path: `${FILE_BASEURL}/${newValue.floor_map_url}.svg`,
},
defaultOption(newValue?.floor_map_name, parsedCoordinates)
defaultOption(newValue.floor_map_name, parsedCoordinates.value)
);
}
});

View File

@ -59,7 +59,12 @@ const searchData = ref({
const getDataSource = async () => {
loading.value = true;
const res = await getAccountUserList(searchData.value);
dataSource.value = res.data;
// webUser
dataSource.value = res.data.filter(
(i) =>
i.userinfo_guid !== "6ac24708-3a40-4199-88c5-22df310cd1a8" &&
i.userinfo_guid !== "B43E3CA7-96DD-4FC7-B6E6-974ACC3B0878"
);
loading.value = false;
};

View File

@ -7,11 +7,10 @@ import {
} from "@/apis/alert";
import useSearchParam from "@/hooks/useSearchParam";
import AlertOutliersTableAddModal from "./AlertOutliersTableAddModal.vue";
import AlertOutliersTimeModal from "./AlertOutliersTimeModal.vue";
import dayjs from "dayjs";
import { useI18n } from "vue-i18n";
const { t } = useI18n();
const { noticeList } = inject("notify_table");
const { noticeList, timesList } = inject("notify_table");
const { searchParams, changeParams } = useSearchParam();
const tableData = ref([]);
@ -111,6 +110,9 @@ const getOutliersData = async () => {
matchedPoints?.factor && item.factor
? matchedPoints?.factor?.find((f) => f.id === item.factor)
: null;
const matchedTime = timesList.value.find(
(t) => t.id === item.schedule_id
);
const warningMethodKeys = item.notices
?.map((noticeValue) => {
const matchedNotice = noticeList.value.find(
@ -128,6 +130,7 @@ const getOutliersData = async () => {
is_bool: matchedPoints ? matchedPoints.is_bool : 1,
factor_name: matchedFactor ? matchedFactor.full_name : "",
warning_method: warningMethodKeys,
warning_time: matchedTime ? matchedTime.schedule_name : "",
};
});
}
@ -163,14 +166,6 @@ const onCancel = () => {
editRecord.value = null;
outliers_add_table_item.close();
};
const openTimeModal = () => {
outliers_time_item.showModal();
};
const onTimeCancel = () => {
outliers_time_item.close();
};
</script>
<template>
@ -183,10 +178,6 @@ const onTimeCancel = () => {
:getData="getOutliersData"
:OptionsData="dev_data"
/>
<AlertOutliersTimeModal
:openModal="openTimeModal"
:onCancel="onTimeCancel"
/>
</div>
<Table :columns="columns" :dataSource="tableData" class="mt-3">
<template #bodyCell="{ record, column, index }">
@ -205,13 +196,7 @@ const onTimeCancel = () => {
<span class="whitespace-pre">{{ record.warning_method }}</span>
</template>
<template v-else-if="column.key === 'warning_time'">
<button
class="btn btn-sm btn-success text-white pb-3"
:disabled="!record.enable"
@click.stop.prevent="() => openTimeModal(record)"
>
{{ $t("alert.time_setting") }}
</button>
<span class="whitespace-pre">{{ record.warning_time }}</span>
</template>
<template v-else>
{{ record[column.key] }}

View File

@ -8,7 +8,7 @@ import * as yup from "yup";
import { useI18n } from "vue-i18n";
const { t } = useI18n();
const { openToast } = inject("app_toast");
const { noticeList } = inject("notify_table");
const { timesList, noticeList } = inject("notify_table");
const { searchParams, changeParams } = useSearchParam();
const props = defineProps({
@ -190,6 +190,15 @@ const closeModal = () => {
>
<template #topLeft>{{ $t("alert.status") }}</template>
</RadioGroup>
<Select :value="formState" class="my-2" selectClass="border-info focus-within:border-info" name="schedule_id"
Attribute="schedule_name" :options="timesList">
<template #topLeft>{{$t("alert.warning_time")}}</template>
<template #topRight><button v-if="formState.schedule_id" class="text-base btn-text-without-border"
@click="() => {formState.schedule_id = null}"><font-awesome-icon
:icon="['fas', 'times']"
class="text-[#a5abb1] me-1"
/>{{$t("alert.clear")}}</button></template>
</Select>
<Select
:value="formState"
class="my-2"

View File

@ -1,14 +1,27 @@
<script setup>
import AlertSubList from "./AlertSubList.vue";
import AlertOutliersTable from "./AlertOutliersTable.vue";
import AlertTimeTable from "./AlertTimeTable.vue";
import AlertNotifyTable from "./AlertNotifyTable.vue";
import { ref, provide, onMounted, watch } from "vue";
import { getNoticeList } from "@/apis/alert";
import { getAlarmScheduleList, getNoticeList } from "@/apis/alert";
import { useI18n } from "vue-i18n";
const { locale } = useI18n();
const timesList = ref([]);
const noticeList = ref([]);
const timesListData = async () => {
const res = await getAlarmScheduleList();
timesList.value = res.data.map((items) => ({
...items,
key:items.id,
schedule_array: JSON.parse(items.schedule_json).map((time, index) => ({
day: index + 1,
time,
})),
}));
};
const NoticeListData = async () => {
const res = await getNoticeList(locale.value);
noticeList.value = res.data;
@ -19,16 +32,18 @@ watch(locale, () => {
});
onMounted(() => {
timesListData();
NoticeListData();
});
provide("notify_table", { noticeList });
provide("notify_table", { timesList, noticeList, timesListData });
</script>
<template>
<div>
<AlertSubList />
<AlertOutliersTable />
<AlertTimeTable />
<AlertNotifyTable />
</div>
</template>

View File

@ -0,0 +1,110 @@
<script setup>
import { ref, inject, onMounted } from "vue";
import { deleteAlarmSchedule } from "@/apis/alert";
import AlertTimeTableAddModal from "./AlertTimeTableAddModal.vue";
import { useI18n } from "vue-i18n";
const { t } = useI18n();
const { openToast, cancelToastOpen } = inject("app_toast");
const tableData = ref([]);
const editRecord = ref(null);
const { timesList,timesListData } = inject("notify_table");
const weekDate = ref({
1: t("alert.sunday"),
2: t("alert.monday"),
3: t("alert.tuesday"),
4: t("alert.wednesday"),
5: t("alert.thursday"),
6: t("alert.friday"),
7: t("alert.saturday"),
});
onMounted(() => {
timesListData();
});
const columns = [
{
title: t("alert.schedule_name"),
key: "schedule_name",
},
{
title: t("alert.schedule_content"),
key: "schedule_content",
},
{
title: t("alert.operation"),
key: "operation",
width: 200,
},
];
const openModal = (record) => {
if (record) {
editRecord.value = record;
} else {
editRecord.value = null;
}
outliers_time_item.showModal();
};
const onCancel = () => {
editRecord.value = null;
outliers_time_item.close();
};
const remove = async (id) => {
openToast("warning", t("msg.sure_to_delete"), "body", async () => {
await cancelToastOpen();
const res = await deleteAlarmSchedule(id);
if (res.isSuccess) {
timesListData();
openToast("success", t("msg.delete_success"));
} else {
openToast("error", res.msg);
}
});
};
</script>
<template>
<div class="flex justify-start items-center mt-10">
<h3 class="text-xl mr-5">{{$t("alert.warning_time")}}</h3>
<AlertTimeTableAddModal :openModal="openModal" :onCancel="onCancel" :editRecord="editRecord" :weekDate="weekDate" />
</div>
<Table :columns="columns" :dataSource="timesList" class="w-3/4 mt-3">
<template #bodyCell="{ record, column, index }">
<template v-if="column.key === 'schedule_content'">
<ul class="text-left">
<li v-for="(item, index) in record.schedule_array" :key="index">
<strong v-if="item.time.length">
<span>{{ weekDate[item.day] }}: </span>
<span v-for="(time, timeIndex) in item.time" :key="timeIndex">
{{ time }}
<span v-if="timeIndex < item.time.length - 1">, </span>
</span>
</strong>
</li>
</ul>
</template>
<template v-else-if="column.key === 'operation'">
<button
class="btn btn-sm btn-success text-white mr-2"
@click.stop.prevent="() => openModal(record)"
>
{{$("button.edit")}}
</button>
<button
class="btn btn-sm btn-error text-white"
@click.stop.prevent="() => remove(record.id)"
>
{{$("button.delete")}}
</button>
</template>
<template v-else>
<pre>{{ record[column.key] }}</pre>
</template>
</template>
</Table>
</template>
<style lang="scss" scoped></style>

View File

@ -1,19 +1,41 @@
<script setup>
import { defineProps, onMounted, onUnmounted, ref, reactive } from "vue";
import { defineProps, onMounted, watch, ref, reactive, inject } from "vue";
import useFormErrorMessage from "@/hooks/useFormErrorMessage";
import { postAlertSchedule } from "@/apis/alert";
import * as yup from "yup";
import { useI18n } from "vue-i18n";
const { t } = useI18n();
const { openToast } = inject("app_toast");
const { timesListData } = inject("notify_table");
const props = defineProps({
openModal: Function,
onCancel: Function,
defaultTimeSection: {
type: Object,
default: () => [],
},
editRecord: Object,
weekDate: Object,
});
let scheme = yup.object({
schedule_name: yup.string().required(t("button.required")),
});
const form = ref(null);
const schedule_array = ref([]);
const formState = ref({
id: 0,
schedule_name: "",
});
const { formErrorMsg, handleSubmit, handleErrorReset } = useFormErrorMessage(
scheme.value
);
const closeModal = () => {
props.onCancel();
formState.value = {
id: 0,
schedule_name: "",
};
clear();
};
@ -43,15 +65,6 @@ const tableHeader = ref([
"22",
"23",
]);
const weekDate = ref({
1: "Mon",
2: "Tue",
3: "Wed",
4: "Thu",
5: "Fri",
6: "Sat",
7: "Sun",
});
const rowUnit = ref([]); //
const timeContent = ref([]); //
@ -79,8 +92,8 @@ const handleMouseMove = (i, day) => {
const initSchedule = () => {
for (let i = 0; i < 7; i++) {
const dayIndex = i + 1;
const defaultTimes = props.defaultTimeSection[dayIndex] || [];
const dayIndex = i;
const defaultTimes = schedule_array.value[dayIndex] || [];
let arr = [];
for (let j = 0; j < 96; j++) {
const timeSlot = j / 4;
@ -206,51 +219,67 @@ const filterTime = (start, end) => {
};
const clear = () => {
rowUnit.value.forEach((item) => {
item.forEach((item1) => {
item1.class = null;
});
schedule_array.value = [];
rowUnit.value = [];
timeContent.value = [];
timeSection.value = [];
timeStr.value = [];
};
const onOk = async () => {
const values = await handleSubmit(scheme, formState.value);
const res = await postAlertSchedule({
...values,
schedule_json: timeSection.value ? JSON.stringify(timeSection.value) : "",
});
timeContent.value.forEach((item) => {
item.arr = [];
});
timeSection.value.forEach((item) => {
item.length = 0;
});
timeStr.value.length = 0;
for (let i = 0; i < 7; i++) {
timeStr.value.push("");
if (res.isSuccess) {
timesListData();
closeModal();
} else {
openToast("error", res.msg, "#outliers_time_item");
}
};
const onOk = async () => {};
onMounted(() => {
watch(
() => props.editRecord,
(newValue) => {
if (newValue) {
formState.value = {
...newValue,
};
schedule_array.value = newValue?.schedule_json
? JSON.parse(newValue?.schedule_json)
: [];
initSchedule();
});
}
}
);
</script>
<template>
<button class="btn btn-sm btn-add mr-3" @click.stop.prevent="openModal">
<font-awesome-icon :icon="['fas', 'plus']" />{{ $t("button.add") }}
</button>
<Modal
id="outliers_time_item"
:open="open"
:onCancel="closeModal"
width="1400"
:title="t('alert.warning_time')"
>
<template #modalTitle>
<p>{{ $t("alert.warning_time") }}</p>
<button class="fixed right-10 top-5" @click.prevent="closeModal">
<font-awesome-icon
:icon="['fas', 'times']"
size="1x"
class="text-[#a5abb1]"
/>
</button>
</template>
<template #modalContent>
<form ref="form">
<Input :value="formState" class="mt-2" name="schedule_name">
<template #topLeft>{{ $t("alert.schedule_name") }}</template>
<template #bottomLeft>
<span class="text-error text-base">
{{ formErrorMsg.schedule_name }}
</span>
</template>
</Input>
<p class="text-light text-lg my-2">{{ $t("alert.schedule_content") }}</p>
<div class="weektime mt-5">
<div class="calendar">
<table class="calendar-table w-full">
@ -295,11 +324,16 @@ onMounted(() => {
</tr>
<tr>
<td colspan="97" class="py-2">
<span class="text-base">{{
<span class="text-base me-2">{{
$t("alert.click_time_period")
}}</span>
<a
@click="clear"
@click="
() => {
clear();
initSchedule();
}
"
class="cursor-pointer text-active text-base"
>
{{ $t("alert.clear") }}
@ -324,6 +358,7 @@ onMounted(() => {
</table>
</div>
</div>
</form>
</template>
<template #modalAction>
<button

View File

@ -1,54 +1,39 @@
<script setup>
import ImmediateDemandChart from "./components/ImmediateDemandChart.vue";
import ElecConsumption from "./components/ElecConsumption.vue";
import UsageInformation from "./components/UsageInformation.vue";
import MonthlyElecBillChart from "./components/MonthlyElecBillChart.vue";
import CarbonEmissionChart from "./components/CarbonEmissionChart.vue";
import BillingDegreeChart from "./components/BillingDegreeChart.vue";
import IntervalBillChart from "./components/IntervalBillChart.vue";
import { getTaipower } from "@/apis/energy";
import { ref, onMounted, provide } from "vue";
import { ref, onMounted, watch } from "vue";
import { useRoute } from "vue-router";
const taipower_data = ref([]);
const getData = async () => {
const res = await getTaipower();
if (res.isSuccess) {
taipower_data.value = res.data;
import EnergyChart from "./components/EnergyChart/EnergyChart.vue";
import EnergyHistoryTable from "./components/EnergyHistoryTable/EnergyHistoryTable.vue";
import EnergyReport from "./components/EnergyReport/EnergyReport.vue";
const route = useRoute();
const currentComponent = ref(null);
const updateComponent = () => {
const { main_system_id, sub_system_id } = route.params;
if (main_system_id === "energy_chart") {
if (sub_system_id === "chart") {
currentComponent.value = EnergyChart;
} else {
currentComponent.value = EnergyHistoryTable;
}
} else if (main_system_id === "energy_report") {
currentComponent.value = EnergyReport;
} else {
currentComponent.value = null;
}
};
onMounted(() => {
getData();
});
onMounted(updateComponent);
provide("energy_data", { taipower_data });
watch(
() => route.params,
() => {
updateComponent();
}
);
</script>
<template>
<div class="flex flex-wrap items-center mb-4">
<div class="w-full xl:w-5/12 lg:w-1/2">
<ElecConsumption />
</div>
<div class="w-full xl:w-7/12 lg:w-1/2">
<ImmediateDemandChart />
</div>
<div class="w-full xl:w-1/3 px-3">
<UsageInformation />
</div>
<div class="w-full xl:w-1/3 px-3">
<MonthlyElecBillChart />
</div>
<div class="w-full xl:w-1/3 px-3">
<CarbonEmissionChart />
</div>
<div class="w-full xl:w-1/3 px-3">
<BillingDegreeChart />
</div>
<div class="w-full xl:w-2/3 px-3">
<IntervalBillChart />
</div>
</div>
<div class="grid gap-4 grid-cols-3"></div>
<component :is="currentComponent" />
</template>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,54 @@
<script setup>
import ImmediateDemandChart from "./ImmediateDemandChart.vue";
import ElecConsumption from "./ElecConsumption.vue";
import UsageInformation from "./UsageInformation.vue";
import MonthlyElecBillChart from "./MonthlyElecBillChart.vue";
import CarbonEmissionChart from "./CarbonEmissionChart.vue";
import BillingDegreeChart from "./BillingDegreeChart.vue";
import IntervalBillChart from "./IntervalBillChart.vue";
import { getTaipower } from "@/apis/energy";
import { ref, onMounted, provide } from "vue";
const taipower_data = ref([]);
const getData = async () => {
const res = await getTaipower();
if (res.isSuccess) {
taipower_data.value = res.data;
}
};
onMounted(() => {
getData();
});
provide("energy_data", { taipower_data });
</script>
<template>
<div class="flex flex-wrap items-center mb-4">
<div class="w-full xl:w-5/12 lg:w-1/2">
<ElecConsumption />
</div>
<div class="w-full xl:w-7/12 lg:w-1/2">
<ImmediateDemandChart />
</div>
<div class="w-full xl:w-1/3 px-3">
<UsageInformation />
</div>
<div class="w-full xl:w-1/3 px-3">
<MonthlyElecBillChart />
</div>
<div class="w-full xl:w-1/3 px-3">
<CarbonEmissionChart />
</div>
<div class="w-full xl:w-1/3 px-3">
<BillingDegreeChart />
</div>
<div class="w-full xl:w-2/3 px-3">
<IntervalBillChart />
</div>
</div>
<div class="grid gap-4 grid-cols-3"></div>
</template>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,127 @@
<script setup>
import { computed, defineProps, inject, ref, watch } from "vue";
import { useRoute } from "vue-router";
import { getHistoryData, getHistoryExportData } from "@/apis/history";
import useSearchParam from "@/hooks/useSearchParam";
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
const { searchParams } = useSearchParam();
const route = useRoute();
const props = defineProps({
form: Object,
});
const { updateTableData } = inject("energy_table_data");
let isToastOpen = ref({
open: false,
content: "",
});
const cancelToastOpen = () => {
isToastOpen.value = {
open: false,
content: "",
};
};
const submit = async (e, type = "") => {
e?.preventDefault();
e?.stopPropagation();
const formData = new FormData(props.form);
let params = {};
for (const pair of formData.entries()) {
console.log(pair[0], pair[1]);
params = { ...params, [pair[0]]: pair[1] };
}
if (type === "export") {
const res = await getHistoryExportData({
type: searchParams.value.selectedType,
...params,
...searchParams.value,
}).catch((err) => {
isToastOpen.value = {
open: true,
content: err.msg,
};
});
} else {
const res = await getHistoryData({
...searchParams.value,
Type: 1,
table_type:route.params.type
});
updateTableData(res.data);
}
};
const isSearchButtonDisabled = computed(() => {
if (
!searchParams.value.Points?.length ||
!searchParams.value.Device_list?.length
) {
return true;
} else {
return false;
}
});
const submitBtns = computed(() => [
{
title: t("button.search"),
key: "submit",
active: false,
onClick: submit,
icon: "search",
btn: "btn-search",
disabled: isSearchButtonDisabled.value,
},
{
title: t("button.export"),
key: "export",
icon: "download",
btn: "btn-export",
active: false,
onClick: (e) => submit(e, "export"),
disabled: isSearchButtonDisabled.value,
},
]);
const once = ref(false);
watch(
searchParams,
(newVal) => {
if (
newVal.Type &&
newVal.Points?.length &&
newVal.sub_system_id &&
newVal.Device_list?.length
) {
if (!once.value) {
once.value = true;
submit();
}
}
},
{
deep: true,
}
);
</script>
<template>
<Toast
:content="isToastOpen.content"
:open="isToastOpen.open"
status="info"
:cancel="cancelToastOpen"
/>
<ButtonGroup class="ml-5" :items="submitBtns" :withLine="false" :withBtnClass="true"/>
</template>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,157 @@
<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 dayjs from "dayjs";
const { t } = useI18n();
const { tableData } = inject("energy_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) => dayjs(value).format("HH:mm"), //
},
data: [],
},
yAxis: {
type: "value",
splitLine: { show: false },
axisLabel: { color: "#ffffff" },
},
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.timestamp).format("YYYY-MM-DD HH:mm")
),
values: item.data.map((d) =>
d.value == "無資料" ? null : parseFloat(d.value)
),
maxValue: parseFloat(item.maxValue),
minValue: parseFloat(item.minValue),
averageValue: parseFloat(item.averageValue),
};
return acc;
}, {});
};
// tableData
watch(
tableData,
(newData) => {
if (newData?.length > 0) {
const formattedData = formatChartData(newData);
const series = Object.keys(formattedData).map((seriesKey, index) => {
const { maxValue, minValue, averageValue } =
formattedData[seriesKey];
return {
name: seriesKey,
type: "line",
data: formattedData[seriesKey].values,
showSymbol: false,
itemStyle: {
color: SECOND_CHART_COLOR[index % SECOND_CHART_COLOR.length],
},
markPoint: {
data: [
maxValue !== null
? { type: "max", name: "Max", value: maxValue }
: null,
minValue !== null
? { type: "min", name: "Min", value: minValue }
: null,
].filter(Boolean),
},
markLine: {
data:
averageValue !== null
? [
{
yAxis: averageValue,
name: "Avg",
},
]
: [],
},
};
});
//
history_chart.value?.chart.setOption(
{
...defaultChartOption,
legend: {
...defaultChartOption.legend,
data: Object.keys(formattedData),
},
xAxis: {
...defaultChartOption.xAxis,
data:
Object.keys(formattedData).length > 0
? formattedData[Object.keys(formattedData)[0]].timestamps
: [],
},
series,
},
true
);
}
},
{ deep: true }
);
</script>
<template>
<LineChart
id="history_chart"
class="min-h-[350px] max-h-fit"
:option="defaultChartOption"
ref="history_chart"
/>
</template>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,170 @@
<script setup>
import Table from "@/components/customUI/Table.vue";
import EnergyDataCahrt from "./EnergyDataCahrt.vue";
import { inject, computed, watch, ref, onMounted } from "vue";
import { useI18n } from "vue-i18n";
import { useRoute } from "vue-router";
const { t } = useI18n();
const route = useRoute();
const { tableData } = inject("energy_table_data");
const routeType = ref(1);
const processedTableData = ref([]); // 使 ref
//
const processData = (data) => {
if (!data || !Array.isArray(data) || data.length === 0) {
return [];
}
return data ? [...data] : [];
};
// 使 watch tableData routeType
watch(
tableData,
(newTableData) => {
processedTableData.value = processData(newTableData);
},
{ immediate: true } //
);
const columns = computed(() => {
const columnMap = {
1: [
{
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",
},
],
2: [
{
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("history.start_time"),
key: "startTime",
},
{
title: t("energy.start_value"),
key: "startValue",
},
{
title: t("history.end_time"),
key: "endTime",
filter: true,
},
{
title: t("energy.end_value"),
key: "endValue",
},
{
title: t("energy.difference"),
key: "diffValue",
},
],
3: [
{
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("energy.power_consumption"),
key: "value",
},
{
title: t("history.start_time"),
key: "startTime",
},
{
title: t("history.end_time"),
key: "endTime",
},
{
title: t("energy.ectricity_classification"),
key: "elecType",
},
{
title: t("energy.ranking"),
key: "rank",
},
],
};
return columnMap[routeType.value] || columnMap[1];
});
watch(
() => route.params.type,
(newType) => {
const type = parseInt(newType);
if (!isNaN(type) && type >= 1 && type <= 3) {
routeType.value = type;
} else {
routeType.value = 1;
}
tableData.value = [];
},
{ immediate: true }
);
</script>
<template>
<Table :columns="columns" :dataSource="processedTableData">
<template #beforeTable>
<EnergyDataCahrt v-if="route.params.type == 1" class="mb-10" />
</template>
</Table>
</template>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,37 @@
<script setup>
import EnergySidebar from "./EnergySidebar.vue";
import EnergySearch from "./EnergySearch.vue";
import EnergyDataTable from "./EnergyDataTable.vue";
import dayjs from "dayjs";
import { ref, provide } from "vue";
const tableData = ref([]);
const deptData = ref([]);
const elecType = ref([]);
const subSystem = ref(null);
const updateTableData = (data) => {
tableData.value = data ? data : [];
};
provide("energy_table_data", {
tableData,
updateTableData,
deptData,
elecType,
subSystem,
});
</script>
<template>
<div class="grid grid-cols-10 gap-2">
<div class="col-span-2">
<EnergySidebar />
</div>
<div class="col-span-8">
<EnergySearch />
<EnergyDataTable />
</div>
</div>
</template>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,245 @@
<script setup>
import {
computed,
ref,
watch,
inject,
onMounted,
onBeforeUnmount,
onBeforeMount,
} from "vue";
import { useRoute } from "vue-router";
import ButtonGroup from "@/components/customUI/ButtonGroup.vue";
import EnergyActionButton from "./EnergyActionButton.vue";
import EnergySearchTime from "./EnergySearchTime.vue";
import useActiveBtn from "@/hooks/useActiveBtn";
import { getEnergySearch } from "@/apis/energy";
import { getDepartmentList, getElecTypeList } from "@/apis/asset";
import useSearchParam from "@/hooks/useSearchParam";
import dayjs from "dayjs";
const { deptData, elecType, subSystem } = inject("energy_table_data");
const { searchParams, changeParams } = useSearchParam();
const route = useRoute();
//
const {
items: sysDeptItems,
changeActiveBtn: changeDeptActiveBtn,
setItems: setDeptItems,
selectedBtn: selectedDeptItems,
} = useActiveBtn("multiple");
//
const {
items: sysElecTypeItems,
changeActiveBtn: changeElecTypeActiveBtn,
setItems: setElecTypeItems,
selectedBtn: selectedElecTypeItems,
} = useActiveBtn("multiple");
//
const {
items: sysMainTagItems,
changeActiveBtn: changeMainSysActiveBtn,
setItems: setMainSysItems,
selectedBtn: selectedMainSysItems,
} = useActiveBtn();
//
const {
items: sysTagItems,
changeActiveBtn: changeSysActiveBtn,
setItems: setSysItems,
selectedBtn: selectedSysItems,
} = useActiveBtn();
//
const {
items: points,
changeActiveBtn: changeActivePoint,
setItems: setPoints,
selectedBtn: selectedPoints,
} = useActiveBtn("multiple");
const getDepartment = async () => {
const res = await getDepartmentList();
const dept = res.data.map((d, index) => ({
...d,
title: d.name,
key: d.id,
active: false,
}));
setDeptItems(dept);
};
const getElecType = async () => {
const res = await getElecTypeList();
const elecType = res.data.map((d, index) => ({
...d,
title: d.name,
key: d.id,
active: false,
}));
setElecTypeItems(elecType);
};
const getSystems = async (type) => {
const res = await getEnergySearch(Number(type));
if (res?.data) {
// main_system_tag
const mainSystemMap = new Map();
res.data.forEach((item) => {
if (!mainSystemMap.has(item.main_system_tag)) {
mainSystemMap.set(item.main_system_tag, {
title: item.full_name,
key: item.main_system_tag,
active: false,
subs: [],
});
}
mainSystemMap.get(item.main_system_tag).subs.push({
title: item.sub.full_name,
key: item.sub.sub_system_tag,
active: false,
points: item.sub.point,
});
});
const mainSystems = Array.from(mainSystemMap.values());
if (mainSystems.length > 0) {
mainSystems[0].active = true;
}
setMainSysItems(mainSystems);
}
};
watch(selectedMainSysItems, (newVal) => {
if (newVal) {
let currentSub = newVal.subs;
if (currentSub && currentSub.length > 0) {
currentSub[0].active = true;
}
setSysItems(currentSub);
setPoints([]);
}
});
watch(selectedSysItems, (newVal, oldVal) => {
if (newVal?.key !== searchParams.value.sub_system_tag) {
let currentPoints = newVal.points;
setPoints(
currentPoints.map((point, index) => ({
title: point.full_name,
key: point.point_system_tag,
active: index == 0,
}))
);
subSystem.value = newVal.key;
}
});
watch(selectedPoints, (newVal, oldVal) => {
changeParams({
...searchParams.value,
Points: newVal.map((d) => d.key),
});
});
watch(selectedDeptItems, (newVal, oldVal) => {
deptData.value = newVal.map((d) => d.key);
});
watch(selectedElecTypeItems, (newVal, oldVal) => {
elecType.value = newVal.map((d) => d.key);
});
watch(
() => route.params.type,
(newVal) => {
if (newVal) {
getSystems(newVal);
}
},
{
immediate: true,
}
);
onMounted(() => {
getDepartment();
getElecType();
});
</script>
<template>
<div class="flex flex-col custom-border p-4 mb-4">
<div class="flex items-center gap-4 mb-4">
<h2 class="text-lg font-bold ps-2 whitespace-nowrap">
{{ $t("assetManagement.department") }} :
</h2>
<ButtonGroup
:items="sysDeptItems"
:withLine="true"
:onclick="
(e, item) => {
changeDeptActiveBtn(item);
}
"
/>
</div>
<div class="flex items-center gap-4 mb-4" v-if="route.params.type == 3">
<h2 class="text-lg font-bold ps-2 whitespace-nowrap">
{{ $t("energy.electricity_classification") }} :
</h2>
<ButtonGroup
:items="sysElecTypeItems"
:withLine="true"
:onclick="
(e, item) => {
changeElecTypeActiveBtn(item);
}
"
/>
</div>
<div class="flex items-center gap-4 mb-4">
<h2 class="text-lg font-bold ps-2 whitespace-nowrap">
{{ $t("history.system_category") }} :
</h2>
<ButtonGroup
:items="sysMainTagItems"
:withLine="true"
:onclick="
(e, item) => {
changeMainSysActiveBtn(item);
}
"
/>
</div>
<div class="flex items-center gap-4 mb-4">
<h2 class="text-lg font-bold ps-2 whitespace-nowrap">
{{ $t("history.device_category") }} :
</h2>
<ButtonGroup
:items="sysTagItems"
:withLine="true"
:onclick="
(e, item) => {
changeSysActiveBtn(item);
}
"
/>
</div>
<div class="flex items-center gap-4">
<h2 class="text-lg font-bold ps-2 whitespace-nowrap">
{{ $t("history.point") }} :
</h2>
<ButtonGroup
:items="points"
:withLine="true"
:onclick="
(e, item) => {
changeActivePoint(item);
}
"
/>
</div>
<EnergySearchTime />
<EnergyActionButton />
</div>
</template>

View File

@ -0,0 +1,116 @@
<script setup>
import { ref, watch, onMounted } from "vue";
import useSearchParam from "@/hooks/useSearchParam";
import dayjs from "dayjs";
import { useI18n } from "vue-i18n";
import { useRoute } from "vue-router";
const { t, locale } = useI18n();
const { searchParams, changeParams } = useSearchParam();
const route = useRoute();
const itemsForStartTime = ref([]);
const itemsForEndTime = ref();
const initializeItems = () => {
itemsForStartTime.value = [
{
key: "Start_date",
value: searchParams.value?.Start_date
? dayjs(searchParams.value?.Start_date)
: dayjs(),
dateFormat: "yyyy-MM-dd",
placeholder: t("history.start_date"),
},
{
key: "Start_time",
value: { hours: 0, minutes: 0, seconds: 0 },
dateFormat: "HH:mm",
timePicker: true,
placeholder: t("history.start_time"),
},
];
itemsForEndTime.value = [
{
key: "End_date",
value: searchParams.value?.End_date
? dayjs(searchParams.value?.End_date)
: dayjs(),
dateFormat: "yyyy-MM-dd",
placeholder: t("history.end_date"),
},
{
key: "End_time",
value: {
hours: dayjs().format("HH"),
minutes: dayjs().format("mm"),
seconds: 0,
},
dateFormat: "HH:mm",
timePicker: true,
placeholder: t("history.end_time"),
},
];
};
watch(
() => route.params.type,
() => {
initializeItems();
},
{
immediate: true,
}
);
watch(locale, () => {
initializeItems();
});
watch(
[itemsForStartTime, itemsForEndTime],
([newStart, newEnd]) => {
if (newStart && newEnd) {
const dates = Object.fromEntries(
[newStart[0], newEnd[0]].map(({ key, value, dateFormat }) => [
key,
dayjs(value).format(dateFormat.toLocaleUpperCase()),
])
);
const times = Object.fromEntries(
[newStart[1], newEnd[1]].map(
({ key, value, dateFormat }, index, arr) => [
key,
`${value.hours}:${value.minutes}`,
]
)
);
changeParams({
...searchParams.value,
...dates,
Start_time: dayjs(`${dates.Start_date} ${times.Start_time}`).format(
"HH:mm"
),
End_time: dayjs(`${dates.End_date} ${times.End_time}`).format("HH:mm"),
});
}
},
{
immediate: true,
deep: true,
}
);
</script>
<template>
<div class="flex items-center gap-4 mb-4">
<h2 class="text-lg font-bold ps-2 whitespace-nowrap">
{{ $t("history.date_range") }} :
</h2>
<DateGroup class="mr-3" :items="itemsForStartTime" :withLine="true" />
<DateGroup :items="itemsForEndTime" :withLine="true" />
</div>
</template>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,261 @@
<script setup>
import Checkbox from "@/components/customUI/Checkbox.vue";
import { computed, ref, watch, inject } from "vue";
import useBuildingStore from "@/stores/useBuildingStore";
import useSearchParam from "@/hooks/useSearchParam";
import { getHistorySideBar } from "@/apis/history";
import { useI18n } from "vue-i18n";
const { t } = useI18n();
const { searchParams, changeParams } = useSearchParam();
const { deptData, elecType, subSystem } = inject("energy_table_data");
const selectedBuilding = ref([]);
const deviceData = ref([]);
const searchTerm = ref(""); //
const activeSearchTerm = ref("");
const getDeviceData = async ({
sub_system_tag,
department_id,
elec_type_id,
}) => {
const res = await getHistorySideBar({
sub_system_tag,
department_id,
elec_type_id,
});
deviceData.value = (res.data || []).map((building) => ({
building_tag: building.building_tag,
building_name: building.building_name,
floors: building.floor_list.map((floor) => ({
floor_tag: floor.device_floor_tag,
floor_name: floor.floor_name,
devices: floor.device_list.map((device) => ({
...device,
key: device.device_number,
})),
})),
}));
selectedBuilding.value = (res.data || []).map((d) => d.building_tag);
changeSelected([res.data[0]?.floor_list[0]?.device_list[0]?.device_number]);
};
const sysIsExisted = (building_tag) => {
return selectedBuilding.value.includes(building_tag);
};
watch(
[() => subSystem.value, () => deptData.value, () => elecType.value],
(newVal) => {
const [sub_system_tag, department_id, elec_type_id] = newVal;
getDeviceData({
sub_system_tag,
department_id,
elec_type_id,
});
},
{
immediate: true,
deep: true,
}
);
const selectedDeviceNumber = computed(() => {
return typeof searchParams.value.Device_list === "string"
? [searchParams.value.Device_list]
: searchParams.value.Device_list || [];
});
const isChecked = (device, checked) => {
if (checked) {
changeSelected([...selectedDeviceNumber.value, device]);
} else {
changeSelected(
selectedDeviceNumber.value.filter(
(device_number) => device_number !== device
)
);
}
};
const checkedBuilding = computed(() => {
let selected = [];
deviceData.value.forEach(({ building_tag, floors }) => {
let allDevices = [];
floors.forEach((floor) => {
allDevices = [...allDevices, ...floor.devices];
});
if (
selectedDeviceNumber.value?.filter(
(d) => typeof d === "string" && d.split("_")[1] === building_tag
).length === allDevices.length
) {
selected.push(building_tag);
}
});
return selected;
});
const buildingCheck = (building_tag) => {
const building = deviceData.value.find(
(d) => d.building_tag === building_tag
);
const allDevicesInBuilding = building.floors.flatMap((floor) =>
floor.devices.map((device) => device.device_number)
);
if (checkedBuilding.value?.includes(building_tag)) {
//
changeSelected(
selectedDeviceNumber.value.filter(
(device_number) => !allDevicesInBuilding.includes(device_number)
)
);
} else {
//
changeSelected([
...new Set([...selectedDeviceNumber.value, ...allDevicesInBuilding]),
]);
}
};
const areAllDevicesCheckedInFloor = (devices) => {
return devices.every((device) =>
selectedDeviceNumber.value.includes(device.device_number)
);
};
const toggleFloorDevices = (devices) => {
const allChecked = areAllDevicesCheckedInFloor(devices);
if (allChecked) {
changeSelected(
selectedDeviceNumber.value.filter(
(device_number) =>
!devices.some((device) => device.device_number === device_number)
)
);
} else {
const deviceNumbers = devices.map((device) => device.device_number);
changeSelected([
...new Set([...selectedDeviceNumber.value, ...deviceNumbers]),
]);
}
};
const changeSelected = (Device_list) => {
changeParams({
...searchParams.value,
Device_list,
});
};
const filteredDeviceData = computed(() => {
if (!activeSearchTerm.value) return deviceData.value;
return deviceData.value
.map((building) => ({
...building,
floors: building.floors
.map((floor) => ({
...floor,
devices: floor.devices.filter((device) =>
device.device_name.includes(activeSearchTerm.value)
),
}))
.filter((floor) => floor.devices.length > 0),
}))
.filter((building) => building.floors.length > 0);
});
const handleSearch = (e) => {
e.preventDefault();
activeSearchTerm.value = searchTerm.value;
};
const handleInput = (e) => {
searchTerm.value = e.target.value;
};
</script>
<template>
<div class="custom-border p-4">
<label
class="input input-bordered bg-transparent rounded-sm flex items-center px-2 border-info focus-within:border-info"
>
<font-awesome-icon
:icon="['fas', 'search']"
class="w-6 h-6 mr-2 text-info"
/>
<input
type="text"
:value="searchTerm"
@input="handleInput"
:placeholder="t('button.enter_text')"
class="text-white bg-transparent w-full"
@keyup.enter="handleSearch"
/>
</label>
<ul class="menu text-lg">
<template
v-for="building in filteredDeviceData"
:key="building.building_tag"
>
<li>
<details :open="selectedBuilding.includes(building.building_tag)">
<summary>
<Checkbox
:title="building.building_name"
:checked="checkedBuilding.includes(building.building_tag)"
@click="() => buildingCheck(building.building_tag)"
/>
</summary>
<ul>
<template v-for="floor in building.floors" :key="floor.floor_tag">
<li>
<details open>
<summary>
<Checkbox
:title="floor.floor_name"
:checked="areAllDevicesCheckedInFloor(floor.devices)"
@click="toggleFloorDevices(floor.devices)"
/>
</summary>
<ul>
<template
v-for="device in floor.devices"
:key="device.device_number"
>
<li class="flex items-start">
<Checkbox
:title="device.device_name"
:checked="
selectedDeviceNumber.includes(
device.device_number
)
"
@click="
() =>
isChecked(
device.device_number,
!selectedDeviceNumber.includes(
device.device_number
)
)
"
/>
</li>
</template>
</ul>
</details>
</li>
</template>
</ul>
</details>
</li>
</template>
</ul>
</div>
</template>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,23 @@
<script setup>
import EnergyReportSearch from "./EnergyReportSearch.vue";
import EnergyReportTable from "./EnergyReportTable.vue";
import dayjs from "dayjs";
import { ref, provide } from "vue";
const tableData = ref([]);
const loading = ref(false);
provide("energy_table_data", {
tableData,
loading,
});
</script>
<template>
<div class="mt-2">
<EnergyReportSearch />
<EnergyReportTable />
</div>
</template>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,186 @@
<script setup>
import { ref, onMounted, watch, inject } from "vue";
import { useRoute } from "vue-router";
import useActiveBtn from "@/hooks/useActiveBtn";
import EnergyReportTimeRange from "./EnergyReportTimeRange.vue";
import {
getDepartmentList,
getAssetFloorList,
getElecTypeList,
} from "@/apis/asset";
import { getAccountUserList } from "@/apis/account";
import { useI18n } from "vue-i18n";
const { t } = useI18n();
const route = useRoute();
const { tableData, loading } = inject("energy_table_data");
const formState = ref({
department: [],
elecType: [],
floor: [],
start_time: null,
end_time: null,
type: 0,
});
//
const {
items: sysDeptItems,
changeActiveBtn: changeDeptActiveBtn,
setItems: setDeptItems,
selectedBtn: selectedDeptItems,
} = useActiveBtn("multiple");
//
const {
items: sysElecItems,
changeActiveBtn: changeElecActiveBtn,
setItems: setElecItems,
selectedBtn: selectedElecItems,
} = useActiveBtn("multiple");
//
const {
items: sysFloorItems,
changeActiveBtn: changeFloorActiveBtn,
setItems: setFloorItems,
selectedBtn: selectedFloorItems,
} = useActiveBtn("multiple");
const getDepartment = async () => {
const res = await getDepartmentList();
const dept = res.data.map((d, index) => ({
...d,
title: d.name,
key: d.id,
active: index === 0,
}));
setDeptItems(dept);
};
const getElecType = async () => {
const res = await getElecTypeList();
const elecType = res.data.map((d, index) => ({
...d,
title: d.name,
key: d.id,
active: index === 0,
}));
setElecItems(elecType);
};
const getFloor = async () => {
const res = await getAssetFloorList();
const Floor = res.data[0]?.floors.map((d, index) => ({
...d,
title: d.full_name,
key: d.floor_guid,
active: index === 0,
}));
setFloorItems(Floor);
};
const onSearch = async () => {
loading.value = true;
const res = await getAccountUserList(formState.value);
tableData.value = res.data;
loading.value = false;
};
//
watch(
selectedDeptItems,
(newVal) => {
formState.value.department = newVal.map((item) => item.key);
},
{ deep: true }
);
//
watch(
selectedElecItems,
(newVal) => {
formState.value.elecType = newVal.map((item) => item.key);
},
{ deep: true }
);
//
watch(
selectedFloorItems,
(newVal) => {
formState.value.floor = newVal.map((item) => item.key);
},
{ deep: true }
);
//
watch(
() => route.params.type,
(newVal) => {
formState.value.type = newVal;
},
{ immediate: true }
);
onMounted(() => {
getDepartment();
getElecType();
getFloor();
});
</script>
<template>
<div class="w-full custom-border px-4 pb-4 mb-4">
<EnergyReportTimeRange :form="formState" />
<div class="flex items-center gap-4 mb-4">
<h2 class="text-lg font-bold whitespace-nowrap">
{{ $t("assetManagement.department") }} :
</h2>
<ButtonGroup
:items="sysDeptItems"
:withLine="true"
:onclick="
(e, item) => {
changeDeptActiveBtn(item);
}
"
/>
</div>
<div class="flex items-center gap-4 mb-4">
<h2 class="text-lg font-bold whitespace-nowrap">
{{ $t("energy.floor") }} :
</h2>
<ButtonGroup
:items="sysFloorItems"
:withLine="true"
:onclick="
(e, item) => {
changeFloorActiveBtn(item);
}
"
/>
</div>
<div class="flex items-center gap-4 mb-4">
<h2 class="text-lg font-bold whitespace-nowrap">
{{ $t("energy.electricity_classification") }} :
</h2>
<ButtonGroup
:items="sysElecItems"
:withLine="true"
:onclick="
(e, item) => {
changeElecActiveBtn(item);
}
"
/>
</div>
<button class="btn btn-search me-4" @click.stop.prevent="onSearch">
<font-awesome-icon :icon="['fas', 'search']" class="text-lg" />
{{ $t("button.search") }}
</button>
<button class="btn btn-export" @click.stop.prevent="search">
<font-awesome-icon :icon="['fas', 'download']" class="text-lg" />
{{ $t("button.export") }}
</button>
</div>
</template>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,33 @@
<script setup>
import Table from "@/components/customUI/Table.vue";
import { inject, computed } from "vue";
import { useI18n } from "vue-i18n";
const { t } = useI18n();
const { tableData, loading } = inject("energy_table_data");
const columns = computed(() => [
{
title: t("history.building_name"),
key: "building_name",
},
{
title: t("energy.floor"),
key: "building_name",
},
{
title: t("history.device_name"),
key: "device_name",
filter: true,
},
{
title: t("energy.subtotal"),
key: "subtotal",
}
]);
</script>
<template>
<Table :columns="columns" :dataSource="tableData" :loading="loading"></Table>
</template>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,129 @@
<script setup>
import { ref, watch, defineProps } from "vue";
import { useRoute } from "vue-router";
import dayjs from "dayjs";
import { useI18n } from "vue-i18n";
const { t, locale } = useI18n();
const route = useRoute();
const dateRange = ref([]);
const props = defineProps({
form:Object,
});
const setDateRange = (type) => {
switch (Number(type)) {
case 1:
dateRange.value = [
{
key: "start_at",
value: dayjs().startOf("month").valueOf(),
dateFormat: "yyyy-MM-dd",
placeholder: t("alert.start_date"),
},
{
key: "end_at",
value: dayjs().endOf("month").valueOf(),
dateFormat: "yyyy-MM-dd",
placeholder: t("alert.end_date"),
},
];
break;
case 2:
dateRange.value = [
{
key: "start_at",
value: dayjs().startOf("week").valueOf(),
dateFormat: "yyyy-MM-dd",
placeholder: t("alert.start_date"),
},
{
key: "end_at",
value: dayjs().endOf("week").valueOf(),
dateFormat: "yyyy-MM-dd",
placeholder: t("alert.end_date"),
},
];
break;
case 3:
dateRange.value = [
{
key: "start_at",
value: dayjs().startOf("year").valueOf(),
dateFormat: "yyyy-MM-dd",
placeholder: t("alert.start_date"),
},
{
key: "end_at",
value: dayjs().endOf("year").valueOf(),
dateFormat: "yyyy-MM-dd",
placeholder: t("alert.end_date"),
},
];
break;
case 4:
dateRange.value = [
{
key: "start_at",
value: dayjs("2024-01-01").valueOf(),
dateFormat: "yyyy-MM-dd",
placeholder: t("alert.start_date"),
},
{
key: "end_at",
value: dayjs().endOf("year").valueOf(),
dateFormat: "yyyy-MM-dd",
placeholder: t("alert.end_date"),
},
];
break;
default:
dateRange.value = [
{
key: "start_at",
value: dayjs("2024-01-01").valueOf(),
dateFormat: "yyyy-MM-dd",
placeholder: t("alert.start_date"),
},
{
key: "end_at",
value: dayjs().endOf("year").valueOf(),
dateFormat: "yyyy-MM-dd",
placeholder: t("alert.end_date"),
},
];
break;
}
};
//
watch(
() => route.params.type,
(newVal) => {
setDateRange(newVal)
},
{ immediate: true }
);
// dateRange
watch(
dateRange,
() => {
if(dateRange.value.length > 0){
props.form.start_time= dayjs(dateRange.value[0].value).format("YYYY-MM-DD");
props.form.end_time= dayjs(dateRange.value[1].value).format("YYYY-MM-DD");
}
},
{ deep: true, immediate:true }
);
</script>
<template>
<div class="flex items-center">
<span class="text-lg font-bold me-4">{{ $t("operation.date") }} : </span>
<DateGroup :items="dateRange" :withLine="true" />
</div>
</template>
<style lang="scss" scoped></style>

View File

@ -13,8 +13,8 @@ const deviceData = ref([]);
const searchTerm = ref(""); //
const activeSearchTerm = ref("");
const getDeviceData = async (sub_tag, renew) => {
const res = await getHistorySideBar(sub_tag);
const getDeviceData = async (sub_system_tag, renew) => {
const res = await getHistorySideBar({sub_system_tag: sub_system_tag});
deviceData.value = res.data.map((building) => ({
building_tag: building.building_tag,
building_name: building.building_name,

View File

@ -32,7 +32,7 @@ watch(
</script>
<template>
<Modal id="system_info_modal" :onCancel="onCancel" width="600" :draggable="!data?.isMobile" :backdrop="false">
<Modal id="system_info_modal" :width="600" :draggable="!data?.isMobile" :backdrop="false">
<template #modalContent>
<SystemInfoModalContent />
</template>

View File

@ -1,50 +1,77 @@
<script setup>
import { ref, computed, inject, watch } from "vue";
import { useI18n } from "vue-i18n";
import dayjs from 'dayjs';
const { t, locale } = useI18n();
import dayjs from "dayjs";
const { selectedDevice, selectedDeviceRealtime } = inject("system_selectedDevice");
const loading = ref(true);
const updatedTime = ref(null);
const data = computed(() => {
const { t } = useI18n();
const { selectedDevice, selectedDeviceRealtime } = inject(
"system_selectedDevice"
);
return selectedDevice.value?.value?.points?.map((d) => ({
const groupedData = computed(() => {
const grouped = {};
if (selectedDeviceRealtime?.value) {
selectedDeviceRealtime.value.forEach((record) => {
const { ori_device_number, time } = record;
if (!grouped[ori_device_number]) {
grouped[ori_device_number] = {
time,
data: [],
};
}
selectedDevice.value.value.points.forEach((d) => {
if (record.point === d.points) {
grouped[ori_device_number].data.push({
...d,
value: selectedDeviceRealtime?.value?.find(({ point }) => point === d.points)?.value || 0
})) || []
value: record.value,
});
watch(selectedDeviceRealtime, (newValue,oldValue) => {
if (newValue) {
loading.value = false;
updatedTime.value = dayjs(new Date()).format("YYYY-MM-DD HH:mm:ss");
}else{
loading.value = true;
}
});
});
}
const columns = [{
title: t("system.attribute"),
key: "full_name"
},
{
title: t("system.value"),
key: "value"
}]
return grouped;
});
</script>
<template>
<span v-if="updatedTime" class="mt-5 text-info">{{$t("operation.updated_time")}} : {{ updatedTime }}</span>
<Table
:loading="loading"
:columns="columns"
:dataSource="data || []"
:withStyle="false"
:pagination="false"
<div v-if="selectedDeviceRealtime">
<div v-for="(group, oriDeviceNumber) in groupedData" :key="oriDeviceNumber">
<p class="mt-5 text-info">
{{ $t("operation.updated_time") }} : {{ group.time }}
</p>
<table class="table mt-2">
<thead>
<tr>
<th
class="border text-white text-lg text-center bg-cyan-600 bg-opacity-30"
>
</Table>
{{ $t("system.attribute") }}
</th>
<th
class="border text-white text-lg text-center bg-cyan-600 bg-opacity-30"
>
{{ $t("system.value") }}
</th>
</tr>
</thead>
<tbody>
<tr v-for="(group, index) in group.data" :key="index">
<td class="border text-white text-lg text-center">
{{ group.full_name }}
</td>
<td class="border text-white text-lg text-center">
{{ group.value }}
</td>
</tr>
</tbody>
</table>
</div>
</div>
<p class="text-2xl text-center my-6" v-else>{{$t("table.no_data")}}</p>
</template>
<style lang="scss" scoped></style>