登入帳號紀錄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_DEVLIST_API = `api/Alarm/GetDevList`; // 取得設備
export const GET_OUTLIERS_POINTS_API = `api/Alarm/GetAlarmPoints`; // 取得點位 export const GET_OUTLIERS_POINTS_API = `api/Alarm/GetAlarmPoints`; // 取得點位
export const POST_OUTLIERS_SETTING_API = `api/Alarm/SaveAlarmSetting`; // 新增與修改 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, POST_ALERT_MEMBER,
DELETE_ALERT_MEMBER, DELETE_ALERT_MEMBER,
GET_NOTICE_LIST_API, GET_NOTICE_LIST_API,
GET_ALERT_SCHEDULE_LIST_API,
POST_ALERT_SCHEDULE,
DELETE_ALERT_SCHEDULE,
} from "./api"; } from "./api";
import instance from "@/util/request"; import instance from "@/util/request";
import apihandler from "@/util/apihandler"; import apihandler from "@/util/apihandler";
@ -156,3 +159,34 @@ export const postOutliersSetting = async (data) => {
code: res.code, 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 GET_ASSET_DEPARTMENT_API = `/AssetManage/GetDepartment`;
export const POST_ASSET_DEPARTMENT_API = `/AssetManage/SaveDepartment`; export const POST_ASSET_DEPARTMENT_API = `/AssetManage/SaveDepartment`;
export const DELETE_ASSET_DEPARTMENT_API = `/AssetManage/DeleteDepartment`; 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, GET_ASSET_DEPARTMENT_API,
POST_ASSET_DEPARTMENT_API, POST_ASSET_DEPARTMENT_API,
DELETE_ASSET_DEPARTMENT_API, DELETE_ASSET_DEPARTMENT_API,
GET_ASSET_ELECTYPE_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";
@ -203,8 +204,8 @@ export const getAssetSubPoint = async (sub_system_tag) => {
}); });
}; };
export const getIOTSchema = async (sub_system_tag, points) => { export const getIOTSchema = async (variable_id) => {
const res = await instance.post(GET_ASSET_IOT_SCHEMA_API, {}); const res = await instance.post(GET_ASSET_IOT_SCHEMA_API, {variable_id});
return apihandler(res.code, res.data, { return apihandler(res.code, res.data, {
msg: res.msg, msg: res.msg,
@ -241,3 +242,12 @@ export const deleteDepartmentItem = async (id) => {
code: res.code, 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_REALTIME_DIST_API = `/api/Energe/GetRealTimeDistribution`;
export const GET_ELECUSE_DAY_API = `/api/Energe/GetElecUseDay`; export const GET_ELECUSE_DAY_API = `/api/Energe/GetElecUseDay`;
export const GET_TAI_POWER_API = `/api/Energe/GetTaipower`; 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_REALTIME_DIST_API,
GET_ELECUSE_DAY_API, GET_ELECUSE_DAY_API,
GET_TAI_POWER_API, GET_TAI_POWER_API,
GET_SIDEBAR_API,
GET_SEARCH_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";
@ -31,4 +33,22 @@ export const getTaipower = async () => {
msg: res.msg, msg: res.msg,
code: res.code, 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 apihandler from "@/util/apiHandler";
import downloadExcel from "@/util/downloadExcel"; 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, { const res = await instance.post(GET_HISTORY_SIDEBAR_API, {
sub_system_tag, sub_system_tag,
department_id,
elec_type_id,
}); });
return apihandler(res.code, res.data, { return apihandler(res.code, res.data, {
@ -42,6 +48,7 @@ export const getHistoryData = async ({
End_time, End_time,
Device_list, Device_list,
Points, Points,
table_type,
}) => { }) => {
/* /*
{ {
@ -62,6 +69,7 @@ export const getHistoryData = async ({
Device_list: Array.isArray(Device_list) ? Device_list : [Device_list], Device_list: Array.isArray(Device_list) ? Device_list : [Device_list],
Points: Array.isArray(Points) ? Points : [Points], Points: Array.isArray(Points) ? Points : [Points],
Type: parseInt(Type), Type: parseInt(Type),
table_type: parseInt(table_type),
}); });
return apihandler(res.code, res.data, { 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=${ document.cookie = `JWT-Authorization=${res.data.token}; Max-Age=${
24 * 60 * 60 * 1000 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, { return apihandler(res.code, res.data, {

View File

@ -118,6 +118,6 @@ const curWidth = computed(() => {
</style> </style>
<style scoped> <style scoped>
.line { .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> </style>

View File

@ -51,7 +51,7 @@ watch(
); );
const updateDataSource = (data) => { const updateDataSource = (data) => {
console.log("update", data); // console.log("update", data);
currentDataSource.value = data; currentDataSource.value = data;
}; };
provide("current_table_data", { provide("current_table_data", {
@ -62,6 +62,7 @@ const sortRule = ref({});
const filterColumn = ref({}); const filterColumn = ref({});
const filterItems = ref({}); const filterItems = ref({});
const selectedFilterItem = ref({}); const selectedFilterItem = ref({});
const searchQuery = ref("");
const toggleFilterModal = (key) => { const toggleFilterModal = (key) => {
let newFilter = Object.assign(filterColumn.value); let newFilter = Object.assign(filterColumn.value);
@ -136,7 +137,10 @@ const form = ref(null);
const onFilter = (key, reset = false) => { const onFilter = (key, reset = false) => {
const formData = new FormData(form.value); const formData = new FormData(form.value);
reset && formData.delete(key); if (reset) {
formData.delete(key);
searchQuery.value = "";
}
for (let [name, value] of formData) { for (let [name, value] of formData) {
console.log(name, value); console.log(name, value);
} }
@ -232,22 +236,38 @@ watch(
" "
@click="() => toggleFilterModal(column.key)" @click="() => toggleFilterModal(column.key)"
/> />
<div <div class="fixed z-50" v-if="filterColumn[column.key]">
class="absolute top-full -left-1/2 z-50"
v-if="filterColumn[column.key]"
>
<div class="card min-w-max bg-body shadow-xl px-10 py-5"> <div class="card min-w-max bg-body shadow-xl px-10 py-5">
<Checkbox <label
v-for="item in filterItems[column.key]" class="input input-bordered bg-transparent rounded-lg flex items-center px-2 mb-4 border-success focus-within:border-success"
:title="item.name" >
:value="item.name" <font-awesome-icon
:key="item.name" :icon="['fas', 'search']"
:name="column.key" class="w-6 h-6 mr-2 text-success"
:checked=" />
selectedFilterItem[column.key].includes(item.name) <input
" type="text"
className="justify-start" :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].filter(
(item) =>
item.name && item.name.includes(searchQuery)
)"
:title="item.name"
:value="item.name"
:key="item.name"
:name="column.key"
:checked="
selectedFilterItem[column.key].includes(item.name)
"
className="justify-start"
/>
</div>
<div class="card-actions mt-4 justify-end"> <div class="card-actions mt-4 justify-end">
<input <input
type="reset" type="reset"
@ -314,7 +334,7 @@ watch(
</form> </form>
<slot name="afterTable"></slot> <slot name="afterTable"></slot>
<Pagination <Pagination
v-if="pagination" v-if="pagination"
:pagination="pagination" :pagination="pagination"
:dataSource="dataSourceStorage" :dataSource="dataSourceStorage"
:sort="sortRule" :sort="sortRule"
@ -327,7 +347,7 @@ watch(
<style lang="css" scoped> <style lang="css" scoped>
/**資料框**/ /**資料框**/
.content-box { .content-box {
@apply border border-info p-1 relative mb-4 bg-transparent ; @apply border border-info p-1 relative mb-4 bg-transparent;
} }
.content-box .table { .content-box .table {

View File

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

View File

@ -82,7 +82,23 @@
"total_elec_cost": "总电费", "total_elec_cost": "总电费",
"carbon_equivalent": "碳排当量", "carbon_equivalent": "碳排当量",
"edit_carbon_emission": "编辑碳排放系数", "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": { "alarm": {
"title": "显示警告", "title": "显示警告",
@ -152,7 +168,16 @@
"choose": "选择", "choose": "选择",
"day_time": "星期/时间", "day_time": "星期/时间",
"click_time_period": "请用滑鼠点击时间段", "click_time_period": "请用滑鼠点击时间段",
"clear": "清空" "clear": "清空",
"sunday": "星期日",
"monday": "星期一",
"tuesday": "星期二",
"wednesday": "星期三",
"thursday": "星期四",
"friday": "星期五",
"saturday": "星期六",
"schedule_name": "时段名称",
"schedule_content": "时段内容"
}, },
"operation": { "operation": {
"title": "运维管理", "title": "运维管理",

View File

@ -13,7 +13,7 @@
"upload": { "upload": {
"title": "選擇一個文件或拖放到這裡", "title": "選擇一個文件或拖放到這裡",
"description": "檔案不超過 10MB", "description": "檔案不超過 10MB",
"formats": "檔案格式" "formats": "檔案格式"
}, },
"dashboard": { "dashboard": {
"yesterday_today": "昨天/今天", "yesterday_today": "昨天/今天",
@ -82,7 +82,23 @@
"total_elec_cost": "總電費", "total_elec_cost": "總電費",
"carbon_equivalent": "碳排當量", "carbon_equivalent": "碳排當量",
"edit_carbon_emission": "編輯碳排放係數", "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": { "alarm": {
"title": "顯示警告", "title": "顯示警告",
@ -150,9 +166,18 @@
"notify_items": "通知項目", "notify_items": "通知項目",
"notify_list": "通知名單", "notify_list": "通知名單",
"choose": "選擇", "choose": "選擇",
"day_time":"星期/時間", "day_time": "星期/時間",
"click_time_period":"請用滑鼠點擊時間段", "click_time_period": "請用滑鼠點擊時間段",
"clear":"清空" "clear": "清空",
"sunday": "星期日",
"monday": "星期一",
"tuesday": "星期二",
"wednesday": "星期三",
"thursday": "星期四",
"friday": "星期五",
"saturday": "星期六",
"schedule_name": "時段名稱",
"schedule_content": "時段內容"
}, },
"operation": { "operation": {
"title": "運維管理", "title": "運維管理",
@ -298,10 +323,10 @@
"download": "下載", "download": "下載",
"confirm": "確認", "confirm": "確認",
"restore": "復原", "restore": "復原",
"stop_edit":"停止修改", "stop_edit": "停止修改",
"start_edit":"開始修改" "start_edit": "開始修改"
}, },
"msg":{ "msg": {
"sure_to_delete": "是否確認刪除該項目?", "sure_to_delete": "是否確認刪除該項目?",
"sure_to_delete_permanent": "是否確認永久刪除該項目?", "sure_to_delete_permanent": "是否確認永久刪除該項目?",
"delete_success": "刪除成功", "delete_success": "刪除成功",

View File

@ -82,7 +82,23 @@
"total_elec_cost": "Total Elec. Cost", "total_elec_cost": "Total Elec. Cost",
"carbon_equivalent": "Carbon Equivalent", "carbon_equivalent": "Carbon Equivalent",
"edit_carbon_emission": "Edit carbon emission coefficient", "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": { "alarm": {
"title": "Warning", "title": "Warning",
@ -152,7 +168,16 @@
"choose": "Choose", "choose": "Choose",
"day_time": "Week/Time", "day_time": "Week/Time",
"click_time_period": "Please click the time period with your mouse", "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": { "operation": {
"title": "Operation And Maintenance Management", "title": "Operation And Maintenance Management",

View File

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

View File

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

View File

@ -1,7 +1,43 @@
<script setup> <script setup>
import { ref, provide, onMounted, watch } from "vue";
import AssetMainList from "./components/AssetMainList.vue"; import AssetMainList from "./components/AssetMainList.vue";
import AssetSubList from "./components/AssetSubList.vue"; import AssetSubList from "./components/AssetSubList.vue";
import AssetTable from "./components/AssetTable.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> </script>
<template> <template>

View File

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

View File

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

View File

@ -102,7 +102,7 @@ const closeModal = () => {
</script> </script>
<template> <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") }} <font-awesome-icon :icon="['fas', 'plus']" />{{ $t("button.add") }}
</button> </button>
<Modal <Modal

View File

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

View File

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

View File

@ -35,13 +35,14 @@ onBeforeMount(() => {
const asset_floor_chart = ref(null); const asset_floor_chart = ref(null);
const currentFloor = ref(null); const currentFloor = ref(null);
const selectedOption = ref("add"); const selectedOption = ref("add");
const parsedCoordinates = ref(null);
const defaultOption = (map, data = []) => { const defaultOption = (map, data = []) => {
// //
const formattedData = data.map((coordinate) => { const formattedData = data.map((coordinate) => {
const coordString = JSON.stringify(coordinate); const coordString = JSON.stringify(coordinate);
return { return {
name: coordString, name: coordString,
value: coordinate, value: coordinate,
itemStyle: { itemStyle: {
color: coordString === formState.value.device_coordinate ? "#0000FF" : "#b02a02", color: coordString === formState.value.device_coordinate ? "#0000FF" : "#b02a02",
@ -73,17 +74,21 @@ const defaultOption = (map, data = []) => {
watch(currentFloor, (newValue) => { watch(currentFloor, (newValue) => {
if (newValue?.floor_map_name) { if (newValue?.floor_map_name) {
const coordinates = totalCoordinates.value[newValue.floor_guid] || []; const coordinates =
if (coordinates.length === 0 || coordinates.every(coord => coord === "")) return; (totalCoordinates.value?.[newValue.floor_guid]?.filter(
const parsedCoordinates = coordinates.map((coord) => { (coord) => coord !== ""
return JSON.parse(coord); ) || []);
});
parsedCoordinates.value = coordinates.length > 0
? coordinates.map((coord) => JSON.parse(coord))
: [];
asset_floor_chart.value.updateSvg( asset_floor_chart.value.updateSvg(
{ {
full_name: newValue?.floor_map_name, full_name: newValue.floor_map_name,
path: `${FILE_BASEURL}/${newValue?.floor_map_url}.svg`, path: `${FILE_BASEURL}/${newValue.floor_map_url}.svg`,
}, },
defaultOption(newValue?.floor_map_name, parsedCoordinates) defaultOption(newValue.floor_map_name, parsedCoordinates.value)
); );
} }
}); });
@ -183,16 +188,16 @@ const onOk = async () => {
}; };
const onDelete = async () => { const onDelete = async () => {
openToast("warning", t("msg.sure_to_delete"), "#asset_add_table_item", async () => { openToast("warning", t("msg.sure_to_delete"), "#asset_add_table_item", async () => {
await cancelToastOpen(); await cancelToastOpen();
const res = await deleteAssetFloor({ const res = await deleteAssetFloor({
floor_guid: formState.value.floor_guid, floor_guid: formState.value.floor_guid,
}); });
if (res.isSuccess) { if (res.isSuccess) {
getFloors(); getFloors();
openToast("success", t("msg.delete_success"), "#asset_add_table_item"); openToast("success", t("msg.delete_success"), "#asset_add_table_item");
} else { } else {
openToast("error", res.msg, "#asset_add_table_item"); openToast("error", res.msg, "#asset_add_table_item");
} }
}); });
}; };
@ -239,18 +244,18 @@ const onCancel = () => {
</button> </button>
</div> </div>
<Input <Input
:value="formState" :value="formState"
width="270" width="270"
name="device_coordinate" name="device_coordinate"
:disabled="true" :disabled="true"
> >
<template #topLeft>{{ $t("assetManagement.device_coordinate") }}</template> <template #topLeft>{{ $t("assetManagement.device_coordinate") }}</template>
<template #bottomLeft <template #bottomLeft
><span class="text-error text-base"> ><span class="text-error text-base">
{{ formErrorMsg.device_coordinate }} {{ formErrorMsg.device_coordinate }}
</span></template </span></template
></Input ></Input
> >
</div> </div>
<div class="relative"> <div class="relative">
<EffectScatter <EffectScatter

View File

@ -59,7 +59,12 @@ const searchData = ref({
const getDataSource = async () => { const getDataSource = async () => {
loading.value = true; loading.value = true;
const res = await getAccountUserList(searchData.value); 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; loading.value = false;
}; };
@ -127,8 +132,8 @@ const removeAccount = async (id) => {
await cancelToastOpen(); await cancelToastOpen();
const res = await delAccount(id); const res = await delAccount(id);
if (res.isSuccess) { if (res.isSuccess) {
getDataSource(); getDataSource();
openToast("success", t("msg.delete_success")); openToast("success", t("msg.delete_success"));
} else { } else {
openToast("error", res.msg); openToast("error", res.msg);
} }

View File

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

View File

@ -8,7 +8,7 @@ import * as yup from "yup";
import { useI18n } from "vue-i18n"; import { useI18n } from "vue-i18n";
const { t } = useI18n(); const { t } = useI18n();
const { openToast } = inject("app_toast"); const { openToast } = inject("app_toast");
const { noticeList } = inject("notify_table"); const { timesList, noticeList } = inject("notify_table");
const { searchParams, changeParams } = useSearchParam(); const { searchParams, changeParams } = useSearchParam();
const props = defineProps({ const props = defineProps({
@ -190,6 +190,15 @@ const closeModal = () => {
> >
<template #topLeft>{{ $t("alert.status") }}</template> <template #topLeft>{{ $t("alert.status") }}</template>
</RadioGroup> </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 <Select
:value="formState" :value="formState"
class="my-2" class="my-2"

View File

@ -1,14 +1,27 @@
<script setup> <script setup>
import AlertSubList from "./AlertSubList.vue"; import AlertSubList from "./AlertSubList.vue";
import AlertOutliersTable from "./AlertOutliersTable.vue"; import AlertOutliersTable from "./AlertOutliersTable.vue";
import AlertTimeTable from "./AlertTimeTable.vue";
import AlertNotifyTable from "./AlertNotifyTable.vue"; import AlertNotifyTable from "./AlertNotifyTable.vue";
import { ref, provide, onMounted, watch } from "vue"; import { ref, provide, onMounted, watch } from "vue";
import { getNoticeList } from "@/apis/alert"; import { getAlarmScheduleList, getNoticeList } from "@/apis/alert";
import { useI18n } from "vue-i18n"; import { useI18n } from "vue-i18n";
const { locale } = useI18n(); const { locale } = useI18n();
const timesList = ref([]);
const noticeList = 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 NoticeListData = async () => {
const res = await getNoticeList(locale.value); const res = await getNoticeList(locale.value);
noticeList.value = res.data; noticeList.value = res.data;
@ -19,16 +32,18 @@ watch(locale, () => {
}); });
onMounted(() => { onMounted(() => {
timesListData();
NoticeListData(); NoticeListData();
}); });
provide("notify_table", { noticeList }); provide("notify_table", { timesList, noticeList, timesListData });
</script> </script>
<template> <template>
<div> <div>
<AlertSubList /> <AlertSubList />
<AlertOutliersTable /> <AlertOutliersTable />
<AlertTimeTable />
<AlertNotifyTable /> <AlertNotifyTable />
</div> </div>
</template> </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> <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"; import { useI18n } from "vue-i18n";
const { t } = useI18n(); const { t } = useI18n();
const { openToast } = inject("app_toast");
const { timesListData } = inject("notify_table");
const props = defineProps({ const props = defineProps({
openModal: Function, openModal: Function,
onCancel: Function, onCancel: Function,
defaultTimeSection: { editRecord: Object,
type: Object, weekDate: Object,
default: () => [],
},
}); });
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 = () => { const closeModal = () => {
props.onCancel(); props.onCancel();
formState.value = {
id: 0,
schedule_name: "",
};
clear(); clear();
}; };
@ -43,15 +65,6 @@ const tableHeader = ref([
"22", "22",
"23", "23",
]); ]);
const weekDate = ref({
1: "Mon",
2: "Tue",
3: "Wed",
4: "Thu",
5: "Fri",
6: "Sat",
7: "Sun",
});
const rowUnit = ref([]); // const rowUnit = ref([]); //
const timeContent = ref([]); // const timeContent = ref([]); //
@ -79,8 +92,8 @@ const handleMouseMove = (i, day) => {
const initSchedule = () => { const initSchedule = () => {
for (let i = 0; i < 7; i++) { for (let i = 0; i < 7; i++) {
const dayIndex = i + 1; const dayIndex = i;
const defaultTimes = props.defaultTimeSection[dayIndex] || []; const defaultTimes = schedule_array.value[dayIndex] || [];
let arr = []; let arr = [];
for (let j = 0; j < 96; j++) { for (let j = 0; j < 96; j++) {
const timeSlot = j / 4; const timeSlot = j / 4;
@ -206,124 +219,146 @@ const filterTime = (start, end) => {
}; };
const clear = () => { const clear = () => {
rowUnit.value.forEach((item) => { schedule_array.value = [];
item.forEach((item1) => { rowUnit.value = [];
item1.class = null; 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) => { if (res.isSuccess) {
item.arr = []; timesListData();
}); closeModal();
} else {
timeSection.value.forEach((item) => { openToast("error", res.msg, "#outliers_time_item");
item.length = 0;
});
timeStr.value.length = 0;
for (let i = 0; i < 7; i++) {
timeStr.value.push("");
} }
}; };
const onOk = async () => {}; watch(
() => props.editRecord,
onMounted(() => { (newValue) => {
initSchedule(); if (newValue) {
}); formState.value = {
...newValue,
};
schedule_array.value = newValue?.schedule_json
? JSON.parse(newValue?.schedule_json)
: [];
initSchedule();
}
}
);
</script> </script>
<template> <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 <Modal
id="outliers_time_item" id="outliers_time_item"
:open="open" :open="open"
:onCancel="closeModal" :onCancel="closeModal"
width="1400" 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> <template #modalContent>
<div class="weektime mt-5"> <form ref="form">
<div class="calendar"> <Input :value="formState" class="mt-2" name="schedule_name">
<table class="calendar-table w-full"> <template #topLeft>{{ $t("alert.schedule_name") }}</template>
<thead class="calendar-head"> <template #bottomLeft>
<tr> <span class="text-error text-base">
<th rowspan="6" class="w-20 py-4"> {{ formErrorMsg.schedule_name }}
{{ $t("alert.day_time") }} </span>
</th> </template>
<th colspan="48">00:00 - 12:00</th> </Input>
<th colspan="48">12:00 - 24:00</th> <p class="text-light text-lg my-2">{{ $t("alert.schedule_content") }}</p>
</tr> <div class="weektime mt-5">
<tr> <div class="calendar">
<td <table class="calendar-table w-full">
colspan="4" <thead class="calendar-head">
v-for="(item, index) in tableHeader" <tr>
:key="index" <th rowspan="6" class="w-20 py-4">
> {{ $t("alert.day_time") }}
{{ item }} </th>
</td> <th colspan="48">00:00 - 12:00</th>
</tr> <th colspan="48">12:00 - 24:00</th>
</thead> </tr>
<tbody id="tableBody"> <tr>
<tr v-for="(day, index) in weekDate" :key="index"> <td
<td>{{ day }}</td> colspan="4"
<td v-for="(item, index) in tableHeader"
v-for="(item, i) in rowUnit[index - 1]"
:key="i"
@mousedown.prevent="handleMouseDown(i, index - 1)"
@mouseup.prevent="handleMouseUp(i, index - 1)"
@mousemove.prevent="handleMouseMove(i, index - 1)"
class="calendar-atom-time"
:class="{
'ui-selected': item.class === 'ui-selected',
'hover-highlight':
downEvent &&
index - 1 >= hoverRange.startDay &&
index - 1 <= hoverRange.endDay &&
i >= hoverRange.startSlot &&
i <= hoverRange.endSlot,
}"
></td>
</tr>
<tr>
<td colspan="97" class="py-2">
<span class="text-base">{{
$t("alert.click_time_period")
}}</span>
<a
@click="clear"
class="cursor-pointer text-active text-base"
>
{{ $t("alert.clear") }}
</a>
</td>
</tr>
<tr>
<td colspan="97" class="timeContent">
<div
v-for="(item, index) in timeStr"
:key="index" :key="index"
v-show="item.length"
> >
<span>{{ weekDate[index + 1] }}: </span> {{ item }}
<strong> </td>
<span>{{ item }}</span> </tr>
</strong> </thead>
</div> <tbody id="tableBody">
</td> <tr v-for="(day, index) in weekDate" :key="index">
</tr> <td>{{ day }}</td>
</tbody> <td
</table> v-for="(item, i) in rowUnit[index - 1]"
:key="i"
@mousedown.prevent="handleMouseDown(i, index - 1)"
@mouseup.prevent="handleMouseUp(i, index - 1)"
@mousemove.prevent="handleMouseMove(i, index - 1)"
class="calendar-atom-time"
:class="{
'ui-selected': item.class === 'ui-selected',
'hover-highlight':
downEvent &&
index - 1 >= hoverRange.startDay &&
index - 1 <= hoverRange.endDay &&
i >= hoverRange.startSlot &&
i <= hoverRange.endSlot,
}"
></td>
</tr>
<tr>
<td colspan="97" class="py-2">
<span class="text-base me-2">{{
$t("alert.click_time_period")
}}</span>
<a
@click="
() => {
clear();
initSchedule();
}
"
class="cursor-pointer text-active text-base"
>
{{ $t("alert.clear") }}
</a>
</td>
</tr>
<tr>
<td colspan="97" class="timeContent">
<div
v-for="(item, index) in timeStr"
:key="index"
v-show="item.length"
>
<span>{{ weekDate[index + 1] }}: </span>
<strong>
<span>{{ item }}</span>
</strong>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div> </div>
</div> </form>
</template> </template>
<template #modalAction> <template #modalAction>
<button <button

View File

@ -1,54 +1,39 @@
<script setup> <script setup>
import ImmediateDemandChart from "./components/ImmediateDemandChart.vue"; import { ref, onMounted, watch } from "vue";
import ElecConsumption from "./components/ElecConsumption.vue"; import { useRoute } from "vue-router";
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";
const taipower_data = ref([]); import EnergyChart from "./components/EnergyChart/EnergyChart.vue";
const getData = async () => { import EnergyHistoryTable from "./components/EnergyHistoryTable/EnergyHistoryTable.vue";
const res = await getTaipower(); import EnergyReport from "./components/EnergyReport/EnergyReport.vue";
if (res.isSuccess) {
taipower_data.value = res.data; 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(() => { onMounted(updateComponent);
getData();
});
provide("energy_data", { taipower_data }); watch(
() => route.params,
() => {
updateComponent();
}
);
</script> </script>
<template> <template>
<div class="flex flex-wrap items-center mb-4"> <component :is="currentComponent" />
<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> </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 searchTerm = ref(""); //
const activeSearchTerm = ref(""); const activeSearchTerm = ref("");
const getDeviceData = async (sub_tag, renew) => { const getDeviceData = async (sub_system_tag, renew) => {
const res = await getHistorySideBar(sub_tag); const res = await getHistorySideBar({sub_system_tag: sub_system_tag});
deviceData.value = res.data.map((building) => ({ deviceData.value = res.data.map((building) => ({
building_tag: building.building_tag, building_tag: building.building_tag,
building_name: building.building_name, building_name: building.building_name,

View File

@ -32,7 +32,7 @@ watch(
</script> </script>
<template> <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> <template #modalContent>
<SystemInfoModalContent /> <SystemInfoModalContent />
</template> </template>

View File

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