MQTT位置路徑修改 | CviBuilding存進localStorage | 樓層與部門存到狀態裡 | 首頁、電價表語言包 |能源管理新增棟別、樓層、部門參數 | 歷史資料新增日期區間 | 樓層頁面deBug | 圖資deBug | MQTT頁面 | 系統監控: 新增棟別樓層部門 以及 spriteDbId的deBug | 電表設定

This commit is contained in:
koko 2025-03-13 16:17:09 +08:00
parent f956402648
commit 8b85e2d67c
47 changed files with 1605 additions and 714 deletions

View File

@ -1,4 +1,4 @@
VITE_API_BASEURL = "https://ibms-cvilux-api.production.mjmtech.com.tw"
VITE_FILE_API_BASEURL = "https://cgems.cvilux-group.com:8088"
VITE_MQTT_BASEURL = "ws://192.168.0.217:8083/mqtt"
VITE_MQTT_BASEURL = "wss://mqttwss.mjm-staging.developers-homelab.net"
VITE_FORGE_BASEURL = "https://cgems.cvilux-group.com:8088/dist"

View File

@ -1,4 +1,4 @@
VITE_API_BASEURL = "https://ibms-cvilux-api.production.mjmtech.com.tw"
VITE_FILE_API_BASEURL = "https://cgems.cvilux-group.com:8088"
VITE_MQTT_BASEURL = "wss://192.168.0.217:8084/mqtt"
VITE_MQTT_BASEURL = "wss://mqttwss.mjm-staging.developers-homelab.net"
VITE_FORGE_BASEURL = "https://cgems.cvilux-group.com:8088/dist"

View File

@ -20,6 +20,11 @@ export const GET_ASSET_IOT_LIST_API = `/AssetManage/GetIOTList`;
export const GET_ASSET_SUB_POINT_API = `/AssetManage/GetSubPoint`;
export const GET_ASSET_IOT_SCHEMA_API = `/AssetManage/GetResponseSchema`;
export const POST_ASSET_IOT_SCHEMA_API = `/AssetManage/SaveResponseSchema`;
export const GET_ASSET_DEVICE_ITEM_API = `/AssetManage/GetDeviceItem`;
export const POST_ASSET_DEVICE_ITEM_API = `/AssetManage/SaveDeviceItem`;
export const DELETE_ASSET_DEVICE_ITEM_API = `/AssetManage/DeleteDeviceItem`;
export const GET_ASSET_DEPARTMENT_API = `/AssetManage/GetDepartment`;
export const POST_ASSET_DEPARTMENT_API = `/AssetManage/SaveDepartment`;
@ -28,3 +33,5 @@ export const DELETE_ASSET_DEPARTMENT_API = `/AssetManage/DeleteDepartment`;
export const GET_ASSET_ELECTYPE_API = `/AssetManage/GetElecType`;
export const POST_ASSET_ELECTYPE_API = `/AssetManage/SaveElecType`;
export const DELETE_ASSET_ELECTYPE_API = `/AssetManage/DeleteElecType`;
export const POST_ASSET_ELEC_SETTING_API = `/AssetManage/SaveAssetSetting`;

View File

@ -15,12 +15,17 @@ import {
POST_ASSET_SINGLE_API,
GET_ASSET_SUB_POINT_API,
GET_ASSET_IOT_SCHEMA_API,
POST_ASSET_IOT_SCHEMA_API,
GET_ASSET_DEVICE_ITEM_API,
POST_ASSET_DEVICE_ITEM_API,
DELETE_ASSET_DEVICE_ITEM_API,
GET_ASSET_DEPARTMENT_API,
POST_ASSET_DEPARTMENT_API,
DELETE_ASSET_DEPARTMENT_API,
GET_ASSET_ELECTYPE_API,
POST_ASSET_ELECTYPE_API,
DELETE_ASSET_ELECTYPE_API
DELETE_ASSET_ELECTYPE_API,
POST_ASSET_ELEC_SETTING_API,
} from "./api";
import instance from "@/util/request";
import apihandler from "@/util/apihandler";
@ -156,8 +161,8 @@ export const deleteAssetItem = async (main_id) => {
});
};
export const getAssetFloorList = async () => {
const res = await instance.post(GET_ASSET_FLOOR_LIST_API);
export const getAssetFloorList = async (building_guid) => {
const res = await instance.post(GET_ASSET_FLOOR_LIST_API, { building_guid });
return apihandler(res.code, res.data, {
msg: res.msg,
@ -207,7 +212,63 @@ export const getAssetSubPoint = async (sub_system_tag) => {
};
export const getIOTSchema = async (variable_id) => {
const res = await instance.post(GET_ASSET_IOT_SCHEMA_API, {variable_id});
const res = await instance.post(GET_ASSET_IOT_SCHEMA_API, { variable_id });
return apihandler(res.code, res.data, {
msg: res.msg,
code: res.code,
});
};
export const postIOTSchema = async ({ name, variable_id, points }) => {
const res = await instance.post(POST_ASSET_IOT_SCHEMA_API, {
name,
variable_id,
points,
});
return apihandler(res.code, res.data, {
msg: res.msg,
code: res.code,
});
};
export const getDeviceItem = async (variable_id) => {
const res = await instance.post(GET_ASSET_DEVICE_ITEM_API, { variable_id });
return apihandler(res.code, res.data, {
msg: res.msg,
code: res.code,
});
};
export const postDeviceItem = async ({
id,
variable_id,
full_name,
points,
decimals,
is_bool,
is_link,
}) => {
const res = await instance.post(POST_ASSET_DEVICE_ITEM_API, {
id,
variable_id,
full_name,
points,
decimals,
is_bool,
is_link,
});
return apihandler(res.code, res.data, {
msg: res.msg,
code: res.code,
});
};
export const deleteDeviceItem = async (id) => {
const res = await instance.post(DELETE_ASSET_DEVICE_ITEM_API, { id });
return apihandler(res.code, res.data, {
msg: res.msg,
@ -274,3 +335,12 @@ export const deleteElecTypeItem = async (id) => {
code: res.code,
});
};
export const postAssetElecSetting = async (formData) => {
const res = await instance.post(POST_ASSET_ELEC_SETTING_API, formData);
return apihandler(res.code, res.data, {
msg: res.msg,
code: res.code,
});
};

View File

@ -11,14 +11,22 @@ import {
GET_CARBON_API,
POST_EDIT_CARBON_API,
GET_TIME_ELEC_API,
POST_TIME_ELEC_API
POST_TIME_ELEC_API,
} from "./api";
import instance, { fileInstance } from "@/util/request";
import apihandler from "@/util/apihandler";
import downloadExcel from "@/util/downloadExcel";
export const getRealTimeDist = async () => {
const res = await instance.post(GET_REALTIME_DIST_API);
export const getRealTimeDist = async ({
building_guid,
department_id_list,
floor_guid_list,
}) => {
const res = await instance.post(GET_REALTIME_DIST_API, {
building_guid,
department_id_list,
floor_guid_list,
});
return apihandler(res.code, res.data, {
msg: res.msg,
@ -26,8 +34,16 @@ export const getRealTimeDist = async () => {
});
};
export const getElecUseDay = async () => {
const res = await instance.post(GET_ELECUSE_DAY_API);
export const getElecUseDay = async ({
building_guid,
department_id_list,
floor_guid_list,
}) => {
const res = await instance.post(GET_ELECUSE_DAY_API,{
building_guid,
department_id_list,
floor_guid_list,
});
return apihandler(res.code, res.data, {
msg: res.msg,
@ -35,8 +51,18 @@ export const getElecUseDay = async () => {
});
};
export const getTaipower = async () => {
const res = await instance.post(GET_TAI_POWER_API);
export const getTaipower = async ({
coefficient,
building_guid,
department_id_list,
floor_guid_list,
}) => {
const res = await instance.post(GET_TAI_POWER_API, {
coefficient,
building_guid,
department_id_list,
floor_guid_list,
});
return apihandler(res.code, res.data, {
msg: res.msg,
@ -118,7 +144,7 @@ export const getExcel = async ({
};
export const getDemand = async (building_guid) => {
const res = await instance.post(GET_DEMAND_API, {building_guid});
const res = await instance.post(GET_DEMAND_API, { building_guid });
return apihandler(res.code, res.data, {
msg: res.msg,
@ -126,13 +152,19 @@ export const getDemand = async (building_guid) => {
});
};
export const postEditDemand = async ({ id, contract, alert, reset, building_guid }) => {
export const postEditDemand = async ({
id,
contract,
alert,
reset,
building_guid,
}) => {
const res = await instance.put(POST_EDIT_DEMAND_API, {
id,
contract,
alert,
reset,
building_guid
building_guid,
});
return apihandler(res.code, res.data, {
@ -142,7 +174,7 @@ export const postEditDemand = async ({ id, contract, alert, reset, building_guid
};
export const getCarbonValue = async (building_guid) => {
const res = await instance.post(GET_CARBON_API, {building_guid});
const res = await instance.post(GET_CARBON_API, { building_guid });
return apihandler(res.code, res.data, {
msg: res.msg,
@ -150,11 +182,15 @@ export const getCarbonValue = async (building_guid) => {
});
};
export const postEditCarbonValue = async ({ id, coefficient, building_guid }) => {
export const postEditCarbonValue = async ({
id,
coefficient,
building_guid,
}) => {
const res = await instance.put(POST_EDIT_CARBON_API, {
id,
coefficient,
building_guid
building_guid,
});
return apihandler(res.code, res.data, {
@ -164,7 +200,7 @@ export const postEditCarbonValue = async ({ id, coefficient, building_guid }) =>
};
export const getTimeElec = async (building_guid) => {
const res = await instance.post(GET_TIME_ELEC_API, {building_guid});
const res = await instance.post(GET_TIME_ELEC_API, { building_guid });
return apihandler(res.code, res.data, {
msg: res.msg,
@ -176,7 +212,7 @@ export const postTimeElec = async ({ sheet, cost, building_guid }) => {
const res = await instance.put(POST_TIME_ELEC_API, {
sheet,
cost,
building_guid
building_guid,
});
return apihandler(res.code, res.data, {

View File

@ -16,11 +16,13 @@ export const getHistorySideBar = async ({
sub_system_tag,
department_id,
elec_type_id,
building_guid,
}) => {
const res = await instance.post(GET_HISTORY_SIDEBAR_API, {
sub_system_tag,
department_id,
elec_type_id,
building_guid,
});
return apihandler(res.code, res.data, {

View File

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

View File

@ -27,7 +27,24 @@
"lastweek_electricity_consumption": "上周用电量",
"one_hour": "1小时",
"four_hour": "4小时",
"eight_hour": "8小时"
"eight_hour": "8小时",
"energy_ranking": "能耗排行",
"last_30_days_energy_trend": "近30天能耗趋势",
"today_energy_consumption": "本日能耗",
"this_month_energy_consumption": "本月能耗",
"relative_energy_consumption": "环比能耗",
"daily_relative_change": "日环比",
"weekly_relative_change": "周环比",
"monthly_relative_change": "月环比",
"yearly_relative_change": "年环比",
"today": "今日",
"yesterday": "昨日",
"this_week": "本周",
"last_week": "上周",
"this_month": "本月",
"last_month": "上月",
"this_year": "今年",
"last_year": "去年"
},
"history": {
"title": "历史资料",
@ -358,12 +375,29 @@
"confirm": "确认",
"restore": "复原",
"stop_edit": "停止修改",
"start_edit": "开始修改"
"start_edit": "开始修改",
"convert": "轉換"
},
"msg": {
"sure_to_delete": "是否确认删除该项目?",
"sure_to_delete_permanent": "是否确认永久删除该项目?",
"delete_success": "删除成功",
"delete_failed": "删除失败"
},
"setting": {
"MQTT_parse": "MQTT 解析",
"schema": "架构",
"point": "点位",
"description": "描述",
"IoT_point_name": "IoT 点位名称",
"IoT_point_code": "IoT 点位代号",
"number_of_decimal_places": "小数位数",
"boolean_value": "布林值",
"hide_point": "点位显示",
"schema_name": "架构名称",
"IoT_point_structure": "IoT点位结构",
"system_point_name": "系统点位名称",
"json_format_text": "请贴上 JSON 格式数据",
"json_click_text": "请在左侧输入JSON并点选转换按钮"
}
}

View File

@ -27,7 +27,24 @@
"lastweek_electricity_consumption": "上週用電量",
"one_hour": "1小時",
"four_hour": "4小時",
"eight_hour": "8小時"
"eight_hour": "8小時",
"energy_ranking": "能耗排行",
"last_30_days_energy_trend": "近30天能耗趨勢",
"today_energy_consumption": "本日能耗",
"this_month_energy_consumption": "本月能耗",
"relative_energy_consumption": "環比能耗",
"daily_relative_change": "日環比",
"weekly_relative_change": "周環比",
"monthly_relative_change": "月環比",
"yearly_relative_change": "年環比",
"today": "今日",
"yesterday": "昨日",
"this_week": "本周",
"last_week": "上周",
"this_month": "本月",
"last_month": "上月",
"this_year": "今年",
"last_year": "去年"
},
"history": {
"title": "歷史資料",
@ -102,12 +119,12 @@
"elec_price_list": "電價表",
"residential": "住宅型",
"standard": "標準型",
"simple_elec_price_two_stage":"簡易型時間電價二段式",
"simple_elec_price_three_stage":"簡易型時間電價三段式",
"classification":"分類",
"summer_months":"夏月",
"non_summer_months":"非夏月",
"time_outside_summer_months":"夏月以外的時間",
"simple_elec_price_two_stage": "簡易型時間電價二段式",
"simple_elec_price_three_stage": "簡易型時間電價三段式",
"classification": "分類",
"summer_months": "夏月",
"non_summer_months": "非夏月",
"time_outside_summer_months": "夏月以外的時間",
"basic_elec_charge": "基本電費",
"charged_per_household": "按戶計收",
"per_household_month": "每戶每月",
@ -358,12 +375,29 @@
"confirm": "確認",
"restore": "復原",
"stop_edit": "停止修改",
"start_edit": "開始修改"
"start_edit": "開始修改",
"convert":"轉換"
},
"msg": {
"sure_to_delete": "是否確認刪除該項目?",
"sure_to_delete_permanent": "是否確認永久刪除該項目?",
"delete_success": "刪除成功",
"delete_failed": "刪除失敗"
},
"setting": {
"MQTT_parse": "MQTT 解析",
"schema":"架構",
"point":"點位",
"description":"描述",
"IoT_point_name":"IoT 點位名稱",
"IoT_point_code":"IoT 點位代號",
"number_of_decimal_places":"小數位數",
"boolean_value":"布林值",
"hide_point":"點位顯示",
"schema_name":"架構名稱",
"IoT_point_structure" :"IoT點位結構",
"system_point_name":"系統點位名稱",
"json_format_text": "請貼上 JSON 格式數據",
"json_click_text": "請在左側輸入JSON並點選轉換按鈕"
}
}

View File

@ -15,6 +15,37 @@
"description": "File size cannot exceed 10MB",
"formats": "File formats"
},
"dashboard": {
"yesterday_today": "Yesterday / Today's",
"elec_consumption_comparison": "Electricity Consumption Comparison",
"elec_consumption_comparison_trend": "Electricity Consumption Comparison Trend",
"electricity_consumption": "electricity consumption",
"today_electricity_consumption": "Todays electricity consumption",
"yesterday_electricity_consumption": "Yesterdays electricity consumption",
"this_last_week": "This Week's / Last Week's",
"thisweek_electricity_consumption": "This weeks electricity consumption",
"lastweek_electricity_consumption": "Last weeks electricity consumption",
"one_hour": "1 hour",
"four_hour": "4 hour",
"eight_hour": "8 hour",
"energy_ranking": "Energy consumption ranking",
"last_30_days_energy_trend": "Energy consumption trend for the past 30 days",
"today_energy_consumption": "Today",
"this_month_energy_consumption": "This month",
"relative_energy_consumption": "Energy consumption trend",
"daily_relative_change": "Daily",
"weekly_relative_change": "Weekly",
"monthly_relative_change": "Monthly",
"yearly_relative_change": "Yearly",
"today": "Today",
"yesterday": "Yesterday",
"this_week": "This week",
"last_week": "Last week",
"this_month": "This month",
"last_month": "Last month",
"this_year": "This year",
"last_year": "Last year"
},
"history": {
"title": "Historical Data",
"building_name": "Building",
@ -33,20 +64,6 @@
"end_date": "End date",
"end_time": "End time"
},
"dashboard": {
"yesterday_today": "Yesterday / Today's",
"elec_consumption_comparison": "Electricity Consumption Comparison",
"elec_consumption_comparison_trend": "Electricity Consumption Comparison Trend",
"electricity_consumption": "electricity consumption",
"today_electricity_consumption": "Todays electricity consumption",
"yesterday_electricity_consumption": "Yesterdays electricity consumption",
"this_last_week": "This Week's / Last Week's",
"thisweek_electricity_consumption": "This weeks electricity consumption",
"lastweek_electricity_consumption": "Last weeks electricity consumption",
"one_hour": "1 hour",
"four_hour": "4 hour",
"eight_hour": "8 hour"
},
"system": {
"status": "Status",
"details": "Details",
@ -358,12 +375,29 @@
"confirm": "Confirm",
"restore": "Restore",
"stop_edit": "Stop editing",
"start_edit": "Start editing"
"start_edit": "Start editing",
"convert": "Convert"
},
"msg": {
"sure_to_delete": "Are you sure to delete this item?",
"sure_to_delete_permanent": "Are you sure you want to permanently delete this item?",
"delete_success": "Delete successfully",
"delete_failed": "Delete failed"
},
"setting": {
"MQTT_parse": "MQTT Parse",
"schema": "Schema",
"point": "Point",
"description": "Description",
"IoT_point_name": "IoT Point Name",
"IoT_point_code": "IoT Point Code",
"number_of_decimal_places": "Number of Decimal Places",
"boolean_value": "Boolean Value",
"hide_point": "Point Display",
"schema_name": "Schema name",
"IoT_point_structure": "IoT Point Structure",
"system_point_name": "System Point Name",
"json_format_text": "Please paste JSON format data",
"json_click_text": "Please enter JSON on the left and click the conversion button"
}
}

View File

@ -61,8 +61,11 @@ import {
faDownload,
faStream,
faSave,
faCrown
faCrown,
faClock,
faCheckCircle
} from "@fortawesome/free-solid-svg-icons";
import { faCircle } from "@fortawesome/free-regular-svg-icons";
/* add icons to the library */
library.add(
@ -124,7 +127,10 @@ library.add(
faDownload,
faStream,
faSave,
faCrown
faCrown,
faClock,
faCheckCircle,
faCircle
);
export default library;

View File

@ -48,13 +48,14 @@ const useBuildingStore = defineStore("buildingInfo", () => {
const res = await getBuildings();
buildings.value = res.data;
if (res.data.length > 0 && !selectedBuilding.value) {
selectedBuilding.value = res.data[0]; // 預設選第一個建築
const storedBuilding = JSON.parse(localStorage.getItem("CviBuilding"));
selectedBuilding.value = storedBuilding || res.data[0]; // 預設選第一個建築
}
};
// 獲取樓層資料
const fetchFloorList = async () => {
const res = await getAssetFloorList();
const fetchFloorList = async (building_guid) => {
const res = await getAssetFloorList(building_guid);
floorList.value = res.data[0]?.floors.map((d) => ({
...d,
title: d.full_name,
@ -75,7 +76,7 @@ const useBuildingStore = defineStore("buildingInfo", () => {
// 當 selectedBuilding 改變時,更新 floorList 和 deptList
watch(selectedBuilding, async (newBuilding) => {
if (newBuilding) {
await Promise.all([fetchFloorList(), fetchDepartmentList()]);
await Promise.all([fetchFloorList(newBuilding.building_guid), fetchDepartmentList()]);
}
});

View File

@ -1,5 +1,5 @@
<script setup>
import { ref, provide, onMounted, watch } from "vue";
import { ref, provide, onMounted, watch, computed } from "vue";
import AssetMainList from "./components/AssetMainList.vue";
import AssetSubList from "./components/AssetSubList.vue";
import AssetTable from "./components/AssetTable.vue";
@ -12,8 +12,6 @@ const { searchParams, changeParams } = useSearchParam();
const companyOptions = ref([]);
const iotSchemaOptions = ref([]);
const elecTypeOptions = ref([]);
const departmentList = ref([]);
const floors = ref([]);
const getCompany = async () => {
const res = await getOperationCompanyList();
companyOptions.value = res.data.map((d) => ({ ...d, key: d.id }));
@ -27,11 +25,12 @@ const getElecType = async () => {
elecTypeOptions.value = res.data.map((d) => ({ ...d, key: d.id }));
};
const departmentList = computed(() => storeBuild.deptList);
const floors = computed(() => storeBuild.floorList);
onMounted(() => {
getCompany();
getElecType();
floors.value = storeBuild.floorList;
departmentList.value = storeBuild.deptList;
});
watch(

View File

@ -9,23 +9,11 @@ import dayjs from "dayjs";
import { useI18n } from "vue-i18n";
const { t } = useI18n();
const { openToast, cancelToastOpen } = inject("app_toast");
const { companyOptions, departmentList, floors } = inject("asset_modal_options");
const FILE_BASEURL = import.meta.env.VITE_FILE_API_BASEURL;
const { searchParams, changeParams } = useSearchParam();
const companyOptions = ref([]);
const getCompany = async () => {
const res = await getOperationCompanyList();
companyOptions.value = res.data.map((d) => ({ ...d, key: d.id }));
};
const floors = ref([]);
const totalCoordinates = ref({});
const getFloors = async () => {
const res = await getAssetFloorList();
floors.value = res.data[0]?.floors.map((d) => ({ ...d, key: d.floor_guid }));
};
const tableData = ref([]);
const getAssetData = async () => {
totalCoordinates.value = {}; // totalCoordinates
@ -45,6 +33,7 @@ const getAssetData = async () => {
floor: floors.value.find(({ floor_guid }) => d.floor_guid === floor_guid)
?.full_name,
company: companyOptions.value.find(({ id }) => d.operation_id === id),
department: departmentList.value.find(({ id }) => d.department_id === id)?.name,
buying_date: d?.buying_date
? dayjs(d?.buying_date).format("YYYY-MM-DD")
: "",
@ -56,17 +45,15 @@ const getAssetData = async () => {
};
onMounted(async () => {
await getCompany();
await getFloors();
getAssetData();
});
const columns = computed(() => [
{
title: t("assetManagement.device_number"),
key: "device_number",
class: "break-all",
},
// {
// title: t("assetManagement.device_number"),
// key: "device_number",
// class: "break-all",
// },
{
title: t("assetManagement.device_name"),
key: "full_name",
@ -83,6 +70,11 @@ const columns = computed(() => [
filter: true,
sort: true,
},
{
title: t("assetManagement.department"),
key: "department",
filter: true,
},
{
title: t("assetManagement.device_coordinate"),
key: "device_coordinate",

View File

@ -2,12 +2,11 @@
import { onMounted, ref, inject, watch, computed } from "vue";
import { useI18n } from "vue-i18n";
import mqtt from "mqtt";
import dayjs from "dayjs";
const { t } = useI18n();
const { openToast, cancelToastOpen } = inject("app_toast");
const { formState } = inject(
"asset_table_modal_form"
);
const { formState } = inject("asset_table_modal_form");
const BASEURL = import.meta.env.VITE_MQTT_BASEURL;
// MQTT
const mqttClient = ref(null); // MQTT
@ -26,10 +25,13 @@ const openModal = () => {
const connectMqtt = () => {
const topic = formState.value.topic || ""; //
const mqttHost = `${BASEURL}`;
const protocol = import.meta.env.MODE === "production" ? "wss" : "ws"; // "ws" "wss"
const protocol = "wss"; // "ws" "wss"
mqttClient.value = mqtt.connect(mqttHost, {
protocol,
reconnectPeriod: 1000, //
username: "admin", // MQTT
password: "mjmadmin@99", // MQTT
port: 443,
});
mqttClient.value.on("connect", () => {
@ -47,7 +49,14 @@ const connectMqtt = () => {
mqttClient.value.on("message", (topic, message) => {
//
receivedMessages.value.push({ topic, message: message.toString() });
const now = dayjs(); // 使 dayjs()
const timestamp = now.format("YYYY-MM-DD HH:mm:ss");
receivedMessages.value.push({
topic,
message: message.toString(),
timestamp: timestamp,
});
clearInterval(timer); //
});
@ -88,7 +97,7 @@ const onCancel = () => {
<template>
<div class="flex w-72">
<Input :value="formState" name="topic" >
<Input :value="formState" name="topic">
<template #topLeft>MQTT Topic</template>
</Input>
<button type="button" class="btn btn-add mt-11 ms-1" @click="openModal">
@ -107,8 +116,14 @@ const onCancel = () => {
:key="index"
class="bg-base-200 rounded-md text-wrap shadow shadow-slate-400 p-4 my-2 me-2"
>
<strong class=" text-base block text-info mb-2">{{ message.topic }} :</strong>
<p class=" text-sm break-words">{{ message.message }}</p>
<strong class="text-base block text-info mb-2"
>{{ message.topic }} :</strong
>
<p class="text-sm break-words">{{ message.message }}</p>
<p class="text-xs text-slate-200 pt-2">
<FontAwesomeIcon :icon="['fas', 'clock']" class="me-1" />
{{ message.timestamp }}
</p>
</li>
</ul>
</div>

View File

@ -2,35 +2,37 @@
import { ref, onMounted } from "vue";
import * as echarts from "echarts";
import BarChart from "@/components/chart/BarChart.vue";
import { useI18n } from "vue-i18n";
const { t } = useI18n();
const chartData = ref([
{
category: "日環比",
category: t("dashboard.daily_relative_change"),
this: 230.68,
last: 377.33,
change: -39,
},
{
category: "周環比",
category: t("dashboard.weekly_relative_change"),
this: 608.01,
last: 2711.09,
change: -78,
},
{
category: "月環比",
category: t("dashboard.monthly_relative_change"),
this: 6473.8,
last: 12701.69,
change: -49,
},
{
category: "年環比",
category: t("dashboard.yearly_relative_change"),
this: 46687.17,
last: null,
change: null,
},
]);
const labels = ["今日", "昨日", "本周", "上周", "本月", "上月", "今年", "去年"];
const labels = [t("dashboard.today"), t("dashboard.yesterday"), t("dashboard.this_week"), t("dashboard.last_week"), t("dashboard.this_month"), t("dashboard.last_month"), t("dashboard.this_year"), t("dashboard.last_year")];
const barWidth = 30; // Set barWidth
const barChartOptions = ref({
@ -161,7 +163,7 @@ const barChartOptions = ref({
<div class="flex flex-wrap">
<div class="w-full chart-data relative px-8 py-3">
<div class="flex flex-wrap items-center justify-between">
<h2 class="font-light">環比能耗</h2>
<h2 class="font-light">{{ $t("dashboard.relative_energy_consumption") }}</h2>
</div>
<div class="h-[180px]">
<BarChart

View File

@ -1,6 +1,7 @@
<script setup>
import { ref } from "vue";
import { useI18n } from "vue-i18n";
const { t } = useI18n();
// -
const mockEnergyData = {
monthly: [
@ -47,9 +48,9 @@ const getCurrentEnergyData = () => {
<div class="state-box">
<!-- 標題和切換按鈕 -->
<div class="flex justify-between items-center mb-4">
<h2 class="font-light w-1/2 relative">當月能耗排行</h2>
<h2 class="font-light relative">{{$t("dashboard.energy_ranking")}}</h2>
<button @click="toggleEnergyType" class="btn btn-info btn-xs">
{{ currentEnergyType === "monthly" ? "本日能耗" : "當月能耗" }}
{{ currentEnergyType === "monthly" ? t("dashboard.today_energy_consumption") : t("dashboard.this_month_energy_consumption") }}
</button>
</div>

View File

@ -1,5 +1,5 @@
<script setup>
import { ref, onMounted } from "vue";
import { ref, onMounted, watch } from "vue";
import * as echarts from "echarts";
import BarChart from "@/components/chart/BarChart.vue";
import { useI18n } from "vue-i18n";
@ -113,24 +113,25 @@ const generateCylinderChartOption = (data) => {
const weekComparisonOption = generateCylinderChartOption(energyData.value);
onMounted(() => {
if (
storeBuild.deptList.length > 0 &&
storeBuild.floorList.length > 0
) {
formState.value = {
...formState.value,
floorId: storeBuild.floorList[0].key,
deptId: storeBuild.deptList[0].key,
};
}
});
watch(
() => [storeBuild.deptList.length, storeBuild.floorList.length],
([deptListLength, floorListLength]) => {
if (deptListLength > 0 && floorListLength > 0) {
formState.value = {
...formState.value,
floorId: storeBuild.floorList[0].key,
deptId: storeBuild.deptList[0].key,
};
}
},
{ immediate: true }
);
</script>
<template>
<div class="w-full chart-data relative px-8 py-1">
<div class="flex flex-wrap items-center justify-between">
<h2 class="font-light">近30天能耗趨勢</h2>
<h2 class="font-light">{{$t("dashboard.last_30_days_energy_trend")}}</h2>
<div class="flex items-center w-52 gap-4">
<Select
:value="formState"

View File

@ -5,121 +5,6 @@ import { twMerge } from "tailwind-merge";
import useBuildingStore from "@/stores/useBuildingStore";
const store = useBuildingStore();
const router = useRouter();
//
const mockData = ref([
{
title: "Air Detection System",
icon: "temperature-high",
isError: false,
main_system_tag: "Dust",
sub_system_tag: "EM",
},
{
title: "Lighting System",
icon: "lightbulb",
isError: false,
main_system_tag: "LS",
sub_system_tag: "ECLS",
},
{
title: "Air Condition System",
icon: "fan",
isError: false,
main_system_tag: "ME",
sub_system_tag: "TH",
},
{
title: "Electricity System",
icon: "bolt",
isError: false,
main_system_tag: "EE",
sub_system_tag: "ECP3",
},
{
title: "Elevator System",
icon: "building",
isError: false,
main_system_tag: null,
sub_system_tag: null,
},
{
title: "High Voltage Switchboard",
icon: "charging-station",
isError: false,
main_system_tag: null,
sub_system_tag: null,
},
{
title: "Low Voltage Switchboard",
icon: "charging-station",
isError: false,
main_system_tag: null,
sub_system_tag: null,
},
{
title: "Water Supply System",
icon: "tint",
isError: false,
main_system_tag: null,
sub_system_tag: null,
},
{
title: "Sewage And Wastewater Equipment",
icon: "water",
isError: false,
main_system_tag: null,
sub_system_tag: null,
},
{
title: "Emergency Generator",
icon: "car-battery",
isError: false,
main_system_tag: null,
sub_system_tag: null,
},
{
title: "Fire Equipment",
icon: "fire-extinguisher",
isError: false,
main_system_tag: null,
sub_system_tag: null,
},
{
title: "CCTV System",
icon: "video",
isError: false,
main_system_tag: null,
sub_system_tag: null,
},
{
title: "Access Control System",
icon: "door-open",
isError: false,
main_system_tag: null,
sub_system_tag: null,
},
{
title: "Shutdown System",
icon: "car",
isError: false,
main_system_tag: null,
sub_system_tag: null,
},
{
title: "Emergency Rescue System",
icon: "exclamation-triangle",
isError: false,
main_system_tag: null,
sub_system_tag: null,
},
{
title: "Air Supply Aand Exhaust System",
icon: "wind",
isError: false,
main_system_tag: null,
sub_system_tag: null,
},
]);
const navigateToSubSystem = (mainSystemId, subSystemId) => {
router.push({

View File

@ -7,7 +7,7 @@ import { getCarbonValue } from "@/apis/energy";
import useBuildingStore from "@/stores/useBuildingStore";
const store = useBuildingStore();
const { t, locale } = useI18n();
const { taipower_data } = inject("energy_data");
const { taipower_data, carbonValue } = inject("energy_data");
const carbonData = ref(null);
const defaultChartOption = ref({
tooltip: {
@ -86,6 +86,7 @@ const getData = async () => {
if (store.selectedBuilding.building_guid) {
const res = await getCarbonValue(store.selectedBuilding.building_guid);
carbonData.value = res.data[0];
carbonValue.value = res.data[0].coefficient;
}
};

View File

@ -1,9 +1,9 @@
<script setup>
import { ref, onMounted, nextTick, computed } from "vue";
import { ref, onMounted, nextTick, watch, inject } from "vue";
import * as echarts from "echarts";
import { getRealTimeDist } from "@/apis/energy";
import { useI18n } from "vue-i18n";
const { search_data } = inject("energy_data");
const { t } = useI18n();
const chartDiv = ref(null);
@ -39,7 +39,7 @@ const chartOption = {
borderWidth: 0,
},
lineStyle: {
color: 'gradient',
color: "gradient",
opacity: 0.7,
curveness: 0.5,
},
@ -47,50 +47,74 @@ const chartOption = {
],
};
const loadData = async () => {
const res = await getRealTimeDist();
if (res.isSuccess) {
const loadData = async (value) => {
const res = await getRealTimeDist(value);
if (res.isSuccess && res.data && res.data.length !== 0) {
const rawData = res.data;
const totalValue = rawData.reduce((acc, item) => acc + item.value, 0);
if (rawData) {
const totalValue = rawData.reduce((acc, item) => acc + item.value, 0);
const sortedData = [...rawData].sort((a, b) => b.value - a.value);
// data
const data = [
{ name: "Total", value: totalValue, percentage: 100 },
...sortedData.map((item) => ({
name: item.key,
const sortedData = [...rawData].sort((a, b) => b.value - a.value);
// data
const data = [
{ name: "Total", value: totalValue, percentage: 100 },
...sortedData.map((item) => ({
name: item.key,
value: item.value,
percentage: item.percentage,
})),
];
// links
const links = sortedData.map((item, index) => ({
source: "Total",
target: item.key,
value: item.value,
percentage: item.percentage,
})),
];
}));
// links
const links = sortedData.map((item, index) => ({
source: "Total",
target: item.key,
value: item.value,
percentage: item.percentage,
}));
const colors = ["#45f4ef", "#17CEE3", "#E4EA00", "#62E39A", "#E9971F", "#E52EFF"];
// chartOption
chartOption.series[0].data = data.map((item, index) => ({
...item,
itemStyle: {
color: colors[index % colors.length], //
},
}));
chartOption.series[0].links = links;
const colors = [
"#45f4ef",
"#17CEE3",
"#E4EA00",
"#62E39A",
"#E9971F",
"#E52EFF",
];
// chartOption
chartOption.series[0].data = data.map((item, index) => ({
...item,
itemStyle: {
color: colors[index % colors.length], //
},
}));
chartOption.series[0].links = links;
//
const myChart = echarts.init(chartDiv.value);
myChart.setOption(chartOption);
}
} else {
//
const myChart = echarts.init(chartDiv.value);
myChart.setOption(chartOption);
echarts.init(chartDiv.value).clear();
}
};
onMounted(() => {
loadData();
});
watch(
search_data,
(newValue, oldValue) => {
if (
newValue.building_guid &&
JSON.stringify(newValue) !== JSON.stringify(oldValue)
) {
loadData(newValue);
}
},
{
immediate: true,
deep: true,
}
);
</script>
<template>

View File

@ -1,5 +1,5 @@
<script setup>
import { ref, onMounted, provide } from "vue";
import { ref, computed, provide, watch } from "vue";
import ImmediateDemandChart from "./ImmediateDemandChart.vue";
import ElecConsumption from "./ElecConsumption.vue";
import UsageInformation from "./UsageInformation.vue";
@ -28,25 +28,80 @@ const {
} = useActiveBtn("multiple");
const taipower_data = ref([]);
const getData = async () => {
const res = await getTaipower();
const carbonValue = ref(null);
const search_data = computed(() => {
return {
coefficient: carbonValue.value,
building_guid: storeBuild.selectedBuilding?.building_guid || null,
department_id_list: selectedDeptItems.value.map((item) => item.key),
floor_guid_list: selectedFloorItems.value.map((item) => item.key),
};
});
const getData = async (value) => {
const res = await getTaipower(value);
if (res.isSuccess) {
taipower_data.value = res.data;
taipower_data.value = res.data
? res.data.sort((a, b) => a.month.localeCompare(b.month))
: [];
}
};
onMounted(() => {
getData();
setDeptItems(storeBuild.deptList);
setFloorItems(storeBuild.floorList);
});
watch(
search_data,
(newValue, oldValue) => {
if (
newValue.building_guid &&
newValue.coefficient &&
JSON.stringify(newValue) !== JSON.stringify(oldValue)
) {
getData(newValue);
}
},
{
immediate: true,
deep: true,
}
);
provide("energy_data", { taipower_data });
watch(
() => storeBuild.floorList,
(newValue) => {
if (newValue) {
const floorList = newValue.map((d) => ({
...d,
active: true,
}));
setFloorItems(floorList);
}
},
{
immediate: true,
}
);
watch(
() => storeBuild.deptList,
(newValue) => {
if (newValue) {
const deptList = newValue.map((d) => ({
...d,
active: true,
}));
setDeptItems(deptList);
}
},
{
immediate: true,
}
);
provide("energy_data", { taipower_data, search_data, carbonValue });
</script>
<template>
<div class="flex flex-wrap items-center mb-4">
<div class="w-full border border-info px-4 py-2 rounded my-3">
<div class="w-full border border-info px-4 py-2 rounded mt-3">
<div class="flex items-center gap-3">
<span class="text-md font-extrabold">{{ $t("energy.floor") }} :</span>
<ButtonGroup

View File

@ -1,9 +1,9 @@
<script setup>
import BarChart from "@/components/chart/BarChart.vue";
import { ref, onMounted, computed } from "vue";
import { ref, watch, computed, inject } from "vue";
import { getElecUseDay } from "@/apis/energy";
import { useI18n } from "vue-i18n";
const { search_data } = inject("energy_data");
const { t } = useI18n();
const dataSource = ref([]);
const dateRange = ref({
@ -107,10 +107,12 @@ const chartOption = computed(() => {
};
});
const loadData = async () => {
const res = await getElecUseDay();
const loadData = async (value) => {
const res = await getElecUseDay(value);
if (res.isSuccess) {
dataSource.value = res.data.map((d) => ({ ...d, key: d.id }));
dataSource.value = res.data
.sort((a, b) => a.time.localeCompare(b.time))
.map((d) => ({ ...d, key: d.id }));
const dates = res.data.map((d) => d.time.split(" ")[0]);
dateRange.value = {
@ -120,9 +122,21 @@ const loadData = async () => {
}
};
onMounted(() => {
loadData();
});
watch(
search_data,
(newValue, oldValue) => {
if (
newValue.building_guid &&
JSON.stringify(newValue) !== JSON.stringify(oldValue)
) {
loadData(newValue);
}
},
{
immediate: true,
deep: true,
}
);
</script>
<template>

View File

@ -19,7 +19,7 @@ const calculateData = () => {
item.month.startsWith(currentYear)
);
const totalElecBills = filteredData.reduce((sum, item) => sum + item.kWh, 0);
const totalElecBills = filteredData.reduce((sum, item) => sum + item.costTotal, 0);
const latestMonthData = filteredData[filteredData.length - 1];
const latestMonth = latestMonthData ? latestMonthData.month : "";
const monthDays = latestMonth ? daysInMonth(latestMonth) : 0;

View File

@ -3,7 +3,7 @@ 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';
import { useI18n } from "vue-i18n";
const { t } = useI18n();
const { searchParams } = useSearchParam();
const route = useRoute();
@ -26,7 +26,6 @@ const cancelToastOpen = () => {
};
};
const submit = async (e, type = "") => {
e?.preventDefault();
e?.stopPropagation();
@ -53,8 +52,13 @@ const submit = async (e, type = "") => {
} else {
const res = await getHistoryData({
...searchParams.value,
Type: 1,
table_type:route.params.type
Type:
route.params.type != 1
? 2
: searchParams.value.Type
? searchParams.value.Type
: 1,
table_type: route.params.type,
});
updateTableData(res.data);
}
@ -82,7 +86,7 @@ const submitBtns = computed(() => [
disabled: isSearchButtonDisabled.value,
},
{
title: t("button.export"),
title: t("button.export"),
key: "export",
icon: "download",
btn: "btn-export",
@ -115,13 +119,18 @@ watch(
</script>
<template>
<Toast
<Toast
:content="isToastOpen.content"
:open="isToastOpen.open"
status="info"
:cancel="cancelToastOpen"
/>
<ButtonGroup class="ml-5" :items="submitBtns" :withLine="false" :withBtnClass="true"/>
<ButtonGroup
class="ml-5"
:items="submitBtns"
:withLine="false"
:withBtnClass="true"
/>
</template>
<style lang="scss" scoped></style>

View File

@ -64,7 +64,7 @@ const formatChartData = (data) => {
const seriesKey = `${item.device_name || ""}_${item.item_name || ""}`;
acc[seriesKey] = {
timestamps: item.data.map((d) =>
dayjs(d.timestamp).format("YYYY-MM-DD HH:mm")
dayjs(d.time).format("YYYY-MM-DD HH:mm")
),
values: item.data.map((d) =>
d.value == "無資料" ? null : parseFloat(d.value)
@ -73,7 +73,6 @@ const formatChartData = (data) => {
minValue: parseFloat(item.minValue),
averageValue: parseFloat(item.averageValue),
};
return acc;
}, {});
};
@ -86,8 +85,7 @@ watch(
const formattedData = formatChartData(newData);
const series = Object.keys(formattedData).map((seriesKey, index) => {
const { maxValue, minValue, averageValue } =
formattedData[seriesKey];
const { maxValue, minValue, averageValue } = formattedData[seriesKey];
return {
name: seriesKey,
type: "line",

View File

@ -65,7 +65,7 @@ const getElecType = async () => {
...d,
title: d.name,
key: d.id,
active: false,
active: true,
}));
setElecTypeItems(elecType);
};
@ -152,8 +152,23 @@ watch(
}
);
watch(
() => storeBuild.deptList,
(newValue) => {
if (newValue) {
const deptList = newValue.map((d) => ({
...d,
active: true,
}));
setDeptItems(deptList);
}
},
{
immediate: true,
}
);
onMounted(() => {
setDeptItems(storeBuild.deptList);
getElecType();
});
</script>

View File

@ -4,14 +4,40 @@ import useSearchParam from "@/hooks/useSearchParam";
import dayjs from "dayjs";
import { useI18n } from "vue-i18n";
import { useRoute } from "vue-router";
import useActiveBtn from "@/hooks/useActiveBtn";
const { t, locale } = useI18n();
const { searchParams, changeParams } = useSearchParam();
const route = useRoute();
const {
items: searchTypeItems,
changeActiveBtn: changeTypeActiveBtn,
setItems: setTypeItems,
selectedBtn: selectedTypeItems,
} = useActiveBtn();
const itemsForStartTime = ref([]);
const itemsForEndTime = ref();
const initializeItems = () => {
setTypeItems([
{
title: t("history.date_range"),
key: 1,
active: searchParams.value.Type
? parseInt(searchParams.value.Type) === 1
: true,
},
{
title: t("history.time_range"),
key: 2,
active: searchParams.value.Type
? parseInt(searchParams.value.Type) === 2
: false,
},
]);
itemsForStartTime.value = [
{
key: "Start_date",
@ -101,6 +127,13 @@ watch(
deep: true,
}
);
watch(selectedTypeItems, (newValue) => {
changeParams({
...searchParams.value,
Type: newValue.key,
});
});
</script>
<template>
@ -108,6 +141,16 @@ watch(
<h2 class="text-lg font-bold ps-2 whitespace-nowrap">
{{ $t("history.date_range") }} :
</h2>
<ButtonGroup
v-if="route.params.type == 1"
:items="searchTypeItems"
:withLine="true"
:onclick="
(e, item) => {
changeTypeActiveBtn(item);
}
"
/>
<DateGroup class="mr-3" :items="itemsForStartTime" :withLine="true" />
<DateGroup :items="itemsForEndTime" :withLine="true" />
</div>

View File

@ -6,13 +6,14 @@ import useSearchParam from "@/hooks/useSearchParam";
import { getHistorySideBar } from "@/apis/history";
import { useI18n } from "vue-i18n";
const { t } = useI18n();
const storeBuild = useBuildingStore();
const buildingGuid = computed(() => storeBuild.selectedBuilding?.building_guid);
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,
@ -22,6 +23,7 @@ const getDeviceData = async ({
sub_system_tag,
department_id,
elec_type_id,
building_guid: buildingGuid.value,
});
deviceData.value = (res.data || []).map((building) => ({
building_tag: building.building_tag,

View File

@ -47,7 +47,8 @@ const getElecType = async () => {
const elecType = res.data.map((d, index) => ({
...d,
title: d.name,
key: d.id
key: d.id,
active: true,
}));
setElecItems(elecType);
};
@ -99,9 +100,39 @@ watch(
{ immediate: true }
);
watch(
() => storeBuild.deptList,
(newValue) => {
if (newValue) {
const deptList = newValue.map((d) => ({
...d,
active: true,
}));
setDeptItems(deptList);
}
},
{
immediate: true,
}
);
watch(
() => storeBuild.floorList,
(newValue) => {
if (newValue) {
const floorList = newValue.map((d) => ({
...d,
active: true,
}));
setFloorItems(floorList);
}
},
{
immediate: true,
}
);
onMounted(() => {
setDeptItems(storeBuild.deptList);
setFloorItems(storeBuild.floorList);
getElecType();
});
</script>

View File

@ -65,7 +65,7 @@ const updateFileList = (files) => {
};
const resetForm = () => {
onSubSysClick(options.value[0].key);
// onSubSysClick(options.value[0].key);
updateFileList([]);
};

View File

@ -122,29 +122,15 @@ const getPoint = async (deviceList) => {
};
watch(
selectedPoints,
(newVal, oldVal) => {
[selectedPoints, selectedDeptItems],
([newPoints, newDeptItems], [oldPoints, oldDeptItems]) => {
changeParams({
...searchParams.value,
Points: newVal.map((d) => d.points),
Points: newPoints.map((d) => d.points),
Dept: newDeptItems.map((d) => d.id),
});
},
{
immediate: true,
}
);
watch(
selectedDeptItems,
(newVal, oldVal) => {
changeParams({
...searchParams.value,
Dept: newVal.map((d) => d.id),
});
},
{
immediate: true,
}
{ immediate: true }
);
const form = ref(null);
@ -154,14 +140,32 @@ watch(searchParams, (newVal, oldValue) => {
(newVal?.Device_list?.length && typeof oldValue.Device_list === "string") ||
(newVal?.Device_list?.length && !oldValue?.Device_list?.length)
) {
getPoint(
typeof newVal.Device_list == "string"
? [newVal.Device_list]
: newVal.Device_list
);
if (newVal?.Device_list[0]!==null){
getPoint(
typeof newVal.Device_list == "string"
? [newVal.Device_list]
: newVal.Device_list
);
}
}
});
watch(
() => store.deptList,
(newValue) => {
if (newValue) {
const deptList = newValue.map((d) => ({
...d,
active: true,
}));
setDeptItems(deptList);
}
},
{
immediate: true,
}
);
onMounted(() => {
setMainSysItems(
store.mainSubSys.map(({ full_name, main_system_tag }, index) => ({
@ -170,13 +174,6 @@ onMounted(() => {
active: index === 0,
}))
);
setDeptItems(
store.deptList.map((d, index) => ({
...d,
title: d.name,
key: d.id,
}))
);
});
onBeforeMount(() => {

View File

@ -5,17 +5,23 @@ 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 storeBuild = useBuildingStore();
const selectedBuilding = ref([]);
const deviceData = ref([]);
const searchTerm = ref(""); //
const activeSearchTerm = ref("");
const getDeviceData = async (sub_system_tag,department_id) => {
const deptArray = department_id? department_id.map(Number):null;
const res = await getHistorySideBar({sub_system_tag: sub_system_tag,department_id:deptArray});
const getDeviceData = async (sub_system_tag, department_id) => {
const deptArray = department_id ? department_id.map(Number) : [];
const res = await getHistorySideBar({
sub_system_tag: sub_system_tag,
department_id: deptArray,
elec_type_id: [],
building_guid: storeBuild.selectedBuilding?.building_guid || null,
});
deviceData.value = res.data.map((building) => ({
building_tag: building.building_tag,
building_name: building.building_name,
@ -30,9 +36,7 @@ const getDeviceData = async (sub_system_tag,department_id) => {
}));
selectedBuilding.value = res.data.map((d) => d.building_tag);
changeSelected(
[res.data[0]?.floor_list[0]?.device_list[0]?.device_number]
);
changeSelected([res.data[0]?.floor_list[0]?.device_list[0]?.device_number]);
};
const sysIsExisted = (building_tag) => {
@ -57,7 +61,7 @@ watch(
newVal.sub_system_tag &&
newVal.sub_system_tag != oldVal?.sub_system_tag
) {
getDeviceData(newVal.sub_system_tag,searchParams.value.Dept);
getDeviceData(newVal.sub_system_tag, searchParams.value.Dept);
}
},
{
@ -69,11 +73,8 @@ watch(
watch(
searchParams,
(newVal, oldVal) => {
if (
newVal.Dept &&
newVal.Dept?.length != oldVal?.Dept?.length
) {
getDeviceData(searchParams.value.sub_system_tag,newVal.Dept);
if (newVal.Dept && newVal.Dept?.length != oldVal?.Dept?.length) {
getDeviceData(searchParams.value.sub_system_tag, newVal.Dept);
}
},
{
@ -182,15 +183,19 @@ const changeSelected = (Device_list, renew = false) => {
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);
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) => {
@ -222,7 +227,10 @@ const handleInput = (e) => {
/>
</label>
<ul class="menu text-lg">
<template v-for="building in filteredDeviceData" :key="building.building_tag">
<template
v-for="building in filteredDeviceData"
:key="building.building_tag"
>
<li>
<details :open="selectedBuilding.includes(building.building_tag)">
<summary>

View File

@ -9,6 +9,7 @@ import Floors from "./components/Floors.vue";
import Building from "./components/Building.vue";
import ElecPriceManagement from "./components/ElecPriceManagement.vue";
import MQTTList from "./components/MQTTList.vue";
import Demand from "./components/Demand.vue";
// import PointList from "./components/PointList.vue";
const route = useRoute();
@ -31,6 +32,8 @@ const updateComponent = () => {
currentComponent.value = ElecPriceManagement;
} else if (sub_system_id === "MQTT_Result") {
currentComponent.value = MQTTList;
} else if (sub_system_id === "Demand") {
currentComponent.value = Demand;
}
};

View File

@ -0,0 +1,169 @@
<script setup>
import { onMounted, ref, inject, computed, watch } from "vue";
import { getAssetList, postAssetElecSetting } from "@/apis/asset";
import { useI18n } from "vue-i18n";
import { twMerge } from "tailwind-merge";
import useBuildingStore from "@/stores/useBuildingStore";
import { getElecTypeList } from "@/apis/asset";
const { t } = useI18n();
const { openToast, cancelToastOpen } = inject("app_toast");
const storeBuild = useBuildingStore();
const departmentList = computed(() => storeBuild.deptList);
const floors = computed(() => storeBuild.floorList);
const elecType = ref([]);
const isEditing = ref(false);
const tableData = ref([]);
const columns = computed(() => [
{
title: t("assetManagement.operation"),
key: "operation",
width: 200,
},
{
title: t("assetManagement.device_number"),
key: "device_number",
class: "break-all",
},
{
title: t("assetManagement.device_name"),
key: "full_name",
filter: true,
class: "break-all",
},
{
title: t("assetManagement.floor"),
key: "floor",
filter: true,
sort: true,
},
{
title: t("assetManagement.department"),
key: "department",
filter: true,
},
{
title: t("energy.electricity_classification"),
key: "elecType",
filter: true,
},
]);
const getElecType = async () => {
const res = await getElecTypeList();
elecType.value = res.data.map((d, index) => ({
...d,
key: d.id,
}));
};
const getAssetData = async () => {
// Electricity Meter P3
const res = await getAssetList(465);
if (res.isSuccess) {
tableData.value = res.data.map((d) => ({
...d,
key: d.id,
floor: floors.value.find(({ floor_guid }) => d.floor_guid === floor_guid)
?.full_name,
department: departmentList.value.find(({ id }) => d.department_id === id)
?.name,
elecType: elecType.value.find(({ id }) => d.elec_type_id === id)?.name,
}));
}
};
const onOk = async () => {
const formData = tableData.value.map((item) => ({
main_id: item.main_id,
is_demand: item.is_demand,
}));
const res = await postAssetElecSetting(formData);
if (res.isSuccess) {
onCancel();
} else {
openToast("error", res.msg);
}
};
const onCancel = async () => {
isEditing.value = false;
await getAssetData();
};
const Check = (id) => {
const items = tableData.value.find((d) => d.main_id === id);
items.is_demand = items.is_demand ? 0 : 1;
};
onMounted(() => {
getElecType();
});
watch(
[floors, departmentList],
() => {
if (floors.value.length > 0 && departmentList.value.length > 0) {
getAssetData();
}
},
{ immediate: true }
);
</script>
<template>
<div class="flex justify-start items-center mt-10 mb-5">
<h3 class="text-xl mr-5">電表</h3>
<button
v-if="!isEditing"
class="btn btn-sm btn-add mr-3"
@click.stop.prevent="isEditing = true"
>
<font-awesome-icon :icon="['fas', 'pencil-alt']" />{{
$t("button.start_edit")
}}
</button>
<template v-else>
<button class="btn btn-sm btn-add mr-3" @click.prevent="onOk()">
<font-awesome-icon :icon="['fas', 'save']" />{{ $t("button.confirm") }}
</button>
<button
class="btn btn-sm btn-outline-info mr-3"
@click.stop.prevent="onCancel()"
>
<font-awesome-icon :icon="['fas', 'times']" />{{ $t("button.cancel") }}
</button>
</template>
</div>
<Table :columns="columns" :dataSource="tableData" class="mt-3">
<template #bodyCell="{ record, column, index }">
<template v-if="column.key === 'operation'">
<template v-if="!isEditing">
<FontAwesomeIcon
v-if="record.is_demand"
:icon="['fas', 'check-circle']"
size="lg"
class="text-gray-300"
/>
<FontAwesomeIcon
v-else
:icon="['far', 'circle']"
size="lg"
class="text-gray-300"
/>
</template>
<template v-else>
<Checkbox
:checked="record.is_demand"
@click="() => Check(record.main_id)"
></Checkbox>
</template>
</template>
<template v-else>
{{ record[column.key] }}
</template>
</template>
</Table>
</template>
<style lang="css" scoped></style>

View File

@ -20,7 +20,7 @@ const onOk = async (sheet, data) => {
getData();
onCancel(sheet);
} else {
openToast("error", res.msg, "#immediate_demand_add_item");
openToast("error", res.msg);
}
};
@ -53,7 +53,7 @@ watch(
<template>
<div class="flex justify-start items-center my-5">
<h3 class="text-xl mr-5">簡易型時間電價二段式</h3>
<h3 class="text-xl mr-5">{{ $t("energy.simple_elec_price_two_stage") }}</h3>
<button
v-if="!sim2isEditing"
class="btn btn-sm btn-add mr-3"
@ -81,16 +81,16 @@ watch(
<table class="">
<thead>
<tr>
<th colspan="6" class="bg-teal-800 bg-opacity-20">分類</th>
<th class="bg-teal-800 bg-opacity-20">夏月<br />(6/1~9/30)</th>
<th class="bg-teal-800 bg-opacity-20">非夏月<br />(夏月以外的時間)</th>
<th colspan="6" class="bg-teal-800 bg-opacity-20">{{ $t("energy.classification") }}</th>
<th class="bg-teal-800 bg-opacity-20">{{ $t("energy.summer_months") }}<br />(6/1~9/30)</th>
<th class="bg-teal-800 bg-opacity-20">{{ $t("energy.non_summer_months") }}<br />({{ $t("energy.time_outside_summer_months") }})</th>
</tr>
</thead>
<tbody>
<tr>
<td class="bg-teal-800 bg-opacity-40">基本電費</td>
<td colspan="4" class="bg-teal-800 bg-opacity-40">按戶計收</td>
<td class="bg-teal-800 bg-opacity-40">每戶每月</td>
<td class="bg-teal-800 bg-opacity-40">{{$t("energy.fixed_elec_cost")}}</td>
<td colspan="4" class="bg-teal-800 bg-opacity-40">{{$t("energy.charged_per_household")}}</td>
<td class="bg-teal-800 bg-opacity-40">{{$t("energy.per_household_month")}}</td>
<td colspan="2" class="bg-teal-800 bg-opacity-40">
<input
type="number"
@ -100,12 +100,12 @@ watch(
</td>
</tr>
<tr>
<td rowspan="6">流動電費</td>
<td rowspan="4">週一~週五</td>
<td rowspan="2" class="bg-rose-600 bg-opacity-70">尖峰時間</td>
<td class="bg-rose-600 bg-opacity-20">夏月</td>
<td rowspan="6">{{$t("energy.var_elec_cost")}}</td>
<td rowspan="4">{{$t("energy.mon_to_friday")}}</td>
<td rowspan="2" class="bg-rose-600 bg-opacity-70">{{$t("energy.peak_hours")}}</td>
<td class="bg-rose-600 bg-opacity-20">{{$t("energy.summer_months")}}</td>
<td class="bg-rose-600 bg-opacity-20">09:00 ~ 24:00</td>
<td rowspan="5">每度</td>
<td rowspan="5">{{$t("energy.price_per_kwh")}}</td>
<td class="bg-rose-600 bg-opacity-20">
<input
type="number"
@ -116,7 +116,7 @@ watch(
<td class="bg-rose-600 bg-opacity-20">-</td>
</tr>
<tr>
<td class="bg-rose-600 bg-opacity-20">非夏月</td>
<td class="bg-rose-600 bg-opacity-20">{{$t("energy.non_summer_months")}}</td>
<td class="bg-rose-600 bg-opacity-20">
06:00 ~ 11:00<br />14:00 ~ 24:00
</td>
@ -130,8 +130,8 @@ watch(
</td>
</tr>
<tr>
<td rowspan="2" class="bg-green-600 bg-opacity-60">離峰時間</td>
<td class="bg-green-600 bg-opacity-20">夏月</td>
<td rowspan="2" class="bg-green-600 bg-opacity-60">{{$t("energy.off_peak_hours")}}</td>
<td class="bg-green-600 bg-opacity-20">{{$t("energy.summer_months")}}</td>
<td class="bg-green-600 bg-opacity-20">00:00 ~ 09:00</td>
<td class="bg-green-600 bg-opacity-20">
<input
@ -143,7 +143,7 @@ watch(
<td class="bg-green-600 bg-opacity-20">-</td>
</tr>
<tr>
<td class="bg-green-600 bg-opacity-20">非夏月</td>
<td class="bg-green-600 bg-opacity-20">{{$t("energy.non_summer_months")}}</td>
<td class="bg-green-600 bg-opacity-20">
00:00 ~ 06:00<br />11:00 ~ 14:00
</td>
@ -157,9 +157,9 @@ watch(
</td>
</tr>
<tr>
<td>週六週日<br />及離峰日</td>
<td class="bg-green-600 bg-opacity-60">離峰時間</td>
<td colspan="2" class="bg-green-600 bg-opacity-20">全日</td>
<td>{{$t("energy.sat_sun_off_peak_days")}}</td>
<td class="bg-green-600 bg-opacity-60">{{$t("energy.off_peak_hours")}}</td>
<td colspan="2" class="bg-green-600 bg-opacity-20">{{$t("energy.all_day")}}</td>
<td class="bg-green-600 bg-opacity-20">
<input
type="number"
@ -176,11 +176,11 @@ watch(
</td>
</tr>
<tr>
<td colspan="4">每月總度數超過2000度之部分</td>
<td>每度</td>
<td colspan="4">{{$t("energy.usage_over_2000kwh")}}</td>
<td>{{$t("energy.price_per_kwh")}}</td>
<td colspan="2" class="!text-start">
<div class="flex items-center">
{{$t("energy.add")}}
<input
type="number"
v-model.number="sim2NewValue[7]"
@ -193,7 +193,7 @@ watch(
</table>
<div class="flex justify-start items-center mt-16">
<h3 class="text-xl mr-5">簡易型時間電價三段式</h3>
<h3 class="text-xl mr-5"> {{$t("energy.simple_elec_price_three_stage")}}</h3>
<button
v-if="!sim3isEditing"
class="btn btn-sm btn-add mr-3"
@ -221,16 +221,16 @@ watch(
<table class="my-5">
<thead>
<tr>
<th colspan="6" class="bg-teal-800 bg-opacity-20">分類</th>
<th class="bg-teal-800 bg-opacity-20">夏月<br />(6/1~9/30)</th>
<th class="bg-teal-800 bg-opacity-20">非夏月<br />(夏月以外的時間)</th>
<th colspan="6" class="bg-teal-800 bg-opacity-20">{{$t("energy.classification")}}</th>
<th class="bg-teal-800 bg-opacity-20">{{$t("energy.summer_months")}}<br />(6/1~9/30)</th>
<th class="bg-teal-800 bg-opacity-20">{{$t("energy.non_summer_months")}}<br />({{$t("energy.time_outside_summer_months")}})</th>
</tr>
</thead>
<tbody>
<tr>
<td class="bg-teal-800 bg-opacity-40">基本電費</td>
<td colspan="4" class="bg-teal-800 bg-opacity-40">按戶計收</td>
<td class="bg-teal-800 bg-opacity-40">每戶每月</td>
<td class="bg-teal-800 bg-opacity-40">{{$t("energy.fixed_elec_cost")}}</td>
<td colspan="4" class="bg-teal-800 bg-opacity-40">{{$t("energy.charged_per_household")}}</td>
<td class="bg-teal-800 bg-opacity-40">{{$t("energy.per_household_month")}}</td>
<td colspan="2" class="bg-teal-800 bg-opacity-40">
<input
type="number"
@ -240,12 +240,12 @@ watch(
</td>
</tr>
<tr>
<td rowspan="7">流動電費</td>
<td rowspan="5">週一~週五</td>
<td class="bg-rose-600 bg-opacity-70">尖峰時間</td>
<td class="bg-rose-600 bg-opacity-20">夏月</td>
<td rowspan="7">{{$t("energy.var_elec_cost")}}</td>
<td rowspan="5">{{$t("energy.mon_to_friday")}}</td>
<td class="bg-rose-600 bg-opacity-70">{{$t("energy.peak_hours")}}</td>
<td class="bg-rose-600 bg-opacity-20">{{$t("energy.summer_months")}}</td>
<td class="bg-rose-600 bg-opacity-20">16:00 ~ 22:00</td>
<td rowspan="6">每度</td>
<td rowspan="6">{{$t("energy.price_per_kwh")}}</td>
<td class="bg-rose-600 bg-opacity-20">
<input
type="number"
@ -256,8 +256,8 @@ watch(
<td class="bg-rose-600 bg-opacity-20">-</td>
</tr>
<tr>
<td rowspan="2" class="bg-yellow-400 bg-opacity-80">半尖峰時間</td>
<td class="bg-yellow-500 bg-opacity-20">夏月</td>
<td rowspan="2" class="bg-yellow-400 bg-opacity-80">{{$t("energy.semi_peak_hours")}}</td>
<td class="bg-yellow-500 bg-opacity-20">{{$t("energy.summer_months")}}</td>
<td class="bg-yellow-500 bg-opacity-20">
09:00 ~ 16:00<br />22:00~24:00
</td>
@ -271,7 +271,7 @@ watch(
<td class="bg-yellow-500 bg-opacity-20">-</td>
</tr>
<tr>
<td class="bg-yellow-500 bg-opacity-20">非夏月</td>
<td class="bg-yellow-500 bg-opacity-20">{{$t("energy.non_summer_months")}}</td>
<td class="bg-yellow-500 bg-opacity-20">
06:00 ~ 11:00<br />14:00 ~ 24:00
</td>
@ -285,8 +285,8 @@ watch(
</td>
</tr>
<tr>
<td rowspan="2" class="bg-green-600 bg-opacity-60">離峰時間</td>
<td class="bg-green-600 bg-opacity-20">夏月</td>
<td rowspan="2" class="bg-green-600 bg-opacity-60">{{$t("energy.off_peak_hours")}}</td>
<td class="bg-green-600 bg-opacity-20">{{$t("energy.summer_months")}}</td>
<td class="bg-green-600 bg-opacity-20">00:00 ~ 09:00</td>
<td class="bg-green-600 bg-opacity-20">
<input
@ -298,7 +298,7 @@ watch(
<td class="bg-green-600 bg-opacity-20">-</td>
</tr>
<tr>
<td class="bg-green-600 bg-opacity-20">非夏月</td>
<td class="bg-green-600 bg-opacity-20">{{$t("energy.non_summer_months")}}</td>
<td class="bg-green-600 bg-opacity-20">
00:00 ~ 06:00<br />11:00 ~ 14:00
</td>
@ -312,9 +312,9 @@ watch(
</td>
</tr>
<tr>
<td>週六週日<br />及離峰日</td>
<td colspan="2" class="bg-green-600 bg-opacity-60">離峰時間</td>
<td class="bg-green-600 bg-opacity-20">全日</td>
<td>{{$t("energy.sat_sun_off_peak_days")}}</td>
<td colspan="2" class="bg-green-600 bg-opacity-60">{{$t("energy.off_peak_hours")}}</td>
<td class="bg-green-600 bg-opacity-20">{{$t("energy.all_day")}}</td>
<td class="bg-green-600 bg-opacity-20">
<input
type="number"
@ -331,11 +331,11 @@ watch(
</td>
</tr>
<tr>
<td colspan="4">每月總度數超過2000度之部分</td>
<td>每度</td>
<td colspan="4">{{$t("energy.usage_over_2000kwh")}}</td>
<td>{{$t("energy.price_per_kwh")}}</td>
<td colspan="2" class="!text-start">
<div class="flex items-center">
{{$t("energy.add")}}
<input
type="number"
v-model.number="sim3NewValue[8]"

View File

@ -53,7 +53,9 @@ watch(
<template>
<div class="flex justify-start items-center my-5">
<h3 class="text-xl mr-5">標準型時間電價二段式</h3>
<h3 class="text-xl mr-5">
{{ $t("energy.standard_time_of_use_tariff_2_stage") }}
</h3>
<button
v-if="!stand2isEditing"
class="btn btn-sm btn-add mr-3"
@ -81,19 +83,33 @@ watch(
<table class="">
<thead>
<tr>
<th colspan="6" class="bg-teal-800 bg-opacity-20">分類</th>
<th class="bg-teal-800 bg-opacity-20">夏月<br />(6/1~9/30)</th>
<th class="bg-teal-800 bg-opacity-20">非夏月<br />(夏月以外的時間)</th>
<th colspan="6" class="bg-teal-800 bg-opacity-20">
{{ $t("energy.classification") }}
</th>
<th class="bg-teal-800 bg-opacity-20">
{{ $t("energy.summer_months") }}<br />(6/1~9/30)
</th>
<th class="bg-teal-800 bg-opacity-20">
{{ $t("energy.non_summer_months") }}<br />({{
$t("energy.time_outside_summer_months")
}})
</th>
</tr>
</thead>
<tbody>
<tr>
<td rowspan="6" class="bg-teal-800 bg-opacity-40">基本電費</td>
<td colspan="2" rowspan="2" class="bg-teal-800 bg-opacity-40">
按戶計收
<td rowspan="6" class="bg-teal-800 bg-opacity-40">
{{ $t("energy.fixed_elec_cost") }}
</td>
<td colspan="2" rowspan="2" class="bg-teal-800 bg-opacity-40">
{{ $t("energy.charged_per_household") }}
</td>
<td colspan="2" class="bg-teal-800 bg-opacity-40">
{{ $t("energy.single_phase") }}
</td>
<td rowspan="2" class="bg-teal-800 bg-opacity-40">
{{ $t("energy.per_household_month") }}
</td>
<td colspan="2" class="bg-teal-800 bg-opacity-40">單相</td>
<td rowspan="2" class="bg-teal-800 bg-opacity-40">每戶每月</td>
<td colspan="2" class="bg-teal-800 bg-opacity-40">
<input
type="number"
@ -103,7 +119,9 @@ watch(
</td>
</tr>
<tr>
<td colspan="2" class="bg-teal-800 bg-opacity-40">三相</td>
<td colspan="2" class="bg-teal-800 bg-opacity-40">
{{ $t("energy.three_phase") }}
</td>
<td colspan="2" class="bg-teal-800 bg-opacity-40">
<input
type="number"
@ -113,8 +131,12 @@ watch(
</td>
</tr>
<tr>
<td colspan="4" class="bg-teal-800 bg-opacity-40">經常契約</td>
<td rowspan="4" class="bg-teal-800 bg-opacity-40">每瓩每月</td>
<td colspan="4" class="bg-teal-800 bg-opacity-40">
{{ $t("energy.frequent_contract") }}
</td>
<td rowspan="4" class="bg-teal-800 bg-opacity-40">
{{ $t("energy.per_kw_per_month") }}
</td>
<td class="bg-teal-800 bg-opacity-40">
<input
type="number"
@ -131,7 +153,9 @@ watch(
</td>
</tr>
<tr>
<td colspan="4" class="bg-teal-800 bg-opacity-40">非夏日契約</td>
<td colspan="4" class="bg-teal-800 bg-opacity-40">
{{ $t("energy.non_summer_contract") }}
</td>
<td class="bg-teal-800 bg-opacity-40">-</td>
<td class="bg-teal-800 bg-opacity-40">
<input
@ -142,7 +166,9 @@ watch(
</td>
</tr>
<tr>
<td colspan="4" class="bg-teal-800 bg-opacity-40">週六半尖峰契約</td>
<td colspan="4" class="bg-teal-800 bg-opacity-40">
{{ $t("energy.saturday_semi_peak_contract") }}
</td>
<td class="bg-teal-800 bg-opacity-40">
<input
type="number"
@ -159,7 +185,9 @@ watch(
</td>
</tr>
<tr>
<td colspan="4" class="bg-teal-800 bg-opacity-40">離峰契約</td>
<td colspan="4" class="bg-teal-800 bg-opacity-40">
{{ $t("energy.off_peak_contract") }}
</td>
<td class="bg-teal-800 bg-opacity-40">
<input
type="number"
@ -176,12 +204,16 @@ watch(
</td>
</tr>
<tr>
<td rowspan="9">流動電費</td>
<td rowspan="4">週一~週五</td>
<td rowspan="2" class="bg-rose-600 bg-opacity-70">尖峰時間</td>
<td class="bg-rose-600 bg-opacity-20">夏月</td>
<td rowspan="9">{{ $t("energy.var_elec_cost") }}</td>
<td rowspan="4">{{ $t("energy.mon_to_friday") }}</td>
<td rowspan="2" class="bg-rose-600 bg-opacity-70">
{{ $t("energy.peak_hours") }}
</td>
<td class="bg-rose-600 bg-opacity-20">
{{ $t("energy.summer_months") }}
</td>
<td class="bg-rose-600 bg-opacity-20">09:00 ~ 24:00</td>
<td rowspan="9">每度</td>
<td rowspan="9">{{ $t("energy.price_per_kwh") }}</td>
<td class="bg-rose-600 bg-opacity-20">
<input
type="number"
@ -192,8 +224,12 @@ watch(
<td class="bg-rose-600 bg-opacity-20">-</td>
</tr>
<tr>
<td class="bg-rose-600 bg-opacity-20">非夏月</td>
<td class="bg-rose-600 bg-opacity-20">06:00 ~ 11:00<br />14:00~24:00</td>
<td class="bg-rose-600 bg-opacity-20">
{{ $t("energy.non_summer_months") }}
</td>
<td class="bg-rose-600 bg-opacity-20">
06:00 ~ 11:00<br />14:00~24:00
</td>
<td class="bg-rose-600 bg-opacity-20">-</td>
<td class="bg-rose-600 bg-opacity-20">
<input
@ -204,8 +240,12 @@ watch(
</td>
</tr>
<tr>
<td rowspan="2" class="bg-green-600 bg-opacity-60">離峰時間</td>
<td class="bg-green-600 bg-opacity-20">夏月</td>
<td rowspan="2" class="bg-green-600 bg-opacity-60">
{{ $t("energy.off_peak_hours") }}
</td>
<td class="bg-green-600 bg-opacity-20">
{{ $t("energy.summer_months") }}
</td>
<td class="bg-green-600 bg-opacity-20">00:00 ~ 09:00</td>
<td class="bg-green-600 bg-opacity-20">
<input
@ -217,7 +257,9 @@ watch(
<td class="bg-green-600 bg-opacity-20">-</td>
</tr>
<tr>
<td class="bg-green-600 bg-opacity-20">非夏月</td>
<td class="bg-green-600 bg-opacity-20">
{{ $t("energy.non_summer_months") }}
</td>
<td class="bg-green-600 bg-opacity-20">
00:00 ~ 06:00<br />
11:00 ~ 14:00
@ -232,9 +274,13 @@ watch(
</td>
</tr>
<tr>
<td rowspan="4">週六</td>
<td rowspan="2" class="bg-yellow-400 bg-opacity-80">半尖峰時間</td>
<td class="bg-yellow-400 bg-opacity-20">夏日</td>
<td rowspan="4">{{ $t("energy.saturday") }}</td>
<td rowspan="2" class="bg-yellow-400 bg-opacity-80">
{{ $t("energy.semi_peak_hours") }}
</td>
<td class="bg-yellow-400 bg-opacity-20">
{{ $t("energy.summer_months") }}
</td>
<td class="bg-yellow-400 bg-opacity-20">09:00 ~ 24:00</td>
<td class="bg-yellow-400 bg-opacity-20">
<input
@ -246,7 +292,9 @@ watch(
<td class="bg-yellow-400 bg-opacity-20">-</td>
</tr>
<tr>
<td class="bg-yellow-400 bg-opacity-20">非夏日</td>
<td class="bg-yellow-400 bg-opacity-20">
{{ $t("energy.non_summer_months") }}
</td>
<td class="bg-yellow-400 bg-opacity-20">
06:00 ~ 11:00<br />
14:00 ~ 24:00
@ -261,8 +309,12 @@ watch(
</td>
</tr>
<tr>
<td rowspan="2" class="bg-green-600 bg-opacity-60">離峰時間</td>
<td class="bg-green-600 bg-opacity-20">夏月</td>
<td rowspan="2" class="bg-green-600 bg-opacity-60">
{{ $t("energy.off_peak_hours") }}
</td>
<td class="bg-green-600 bg-opacity-20">
{{ $t("energy.summer_months") }}
</td>
<td class="bg-green-600 bg-opacity-20">00:00 ~ 09:00</td>
<td class="bg-green-600 bg-opacity-20">
<input
@ -274,8 +326,12 @@ watch(
<td class="bg-green-600 bg-opacity-20">-</td>
</tr>
<tr>
<td class="bg-green-600 bg-opacity-20">非夏月</td>
<td class="bg-green-600 bg-opacity-20">00:00 ~ 06:00<br />11:00 ~ 14:00</td>
<td class="bg-green-600 bg-opacity-20">
{{ $t("energy.non_summer_months") }}
</td>
<td class="bg-green-600 bg-opacity-20">
00:00 ~ 06:00<br />11:00 ~ 14:00
</td>
<td class="bg-green-600 bg-opacity-20">-</td>
<td class="bg-green-600 bg-opacity-20">
<input
@ -286,9 +342,13 @@ watch(
</td>
</tr>
<tr>
<td>週六週日<br />及離峰日</td>
<td class="bg-green-600 bg-opacity-60">離峰時間</td>
<td colspan="2" class="bg-green-600 bg-opacity-20">全日</td>
<td>{{ $t("energy.sat_sun_off_peak_days") }}</td>
<td class="bg-green-600 bg-opacity-60">
{{ $t("energy.off_peak_hours") }}
</td>
<td colspan="2" class="bg-green-600 bg-opacity-20">
{{ $t("energy.all_day") }}
</td>
<td class="bg-green-600 bg-opacity-20">
<input
type="number"
@ -308,7 +368,9 @@ watch(
</table>
<div class="flex justify-start items-center mt-14">
<h3 class="text-xl mr-5">標準型時間電價三段式</h3>
<h3 class="text-xl mr-5">
{{ $t("energy.standard_time_of_use_tariff_3_stage") }}
</h3>
<button
v-if="!stand3isEditing"
class="btn btn-sm btn-add mr-3"
@ -336,17 +398,33 @@ watch(
<table class="my-5">
<thead>
<tr>
<th colspan="6" class="bg-teal-800 bg-opacity-20">分類</th>
<th class="bg-teal-800 bg-opacity-20">夏月<br />(6/1~9/30)</th>
<th class="bg-teal-800 bg-opacity-20">非夏月<br />(夏月以外的時間)</th>
<th colspan="6" class="bg-teal-800 bg-opacity-20">
{{ $t("energy.classification") }}
</th>
<th class="bg-teal-800 bg-opacity-20">
{{ $t("energy.summer_months") }}<br />(6/1~9/30)
</th>
<th class="bg-teal-800 bg-opacity-20">
{{ $t("energy.non_summer_months") }}<br />({{
$t("energy.time_outside_summer_months")
}})
</th>
</tr>
</thead>
<tbody>
<tr>
<td rowspan="6" class="bg-teal-800 bg-opacity-40">基本電費</td>
<td colspan="2" rowspan="2" class="bg-teal-800 bg-opacity-40">按戶計收</td>
<td colspan="2" class="bg-teal-800 bg-opacity-40">單相</td>
<td rowspan="2" class="bg-teal-800 bg-opacity-40">每戶每月</td>
<td rowspan="6" class="bg-teal-800 bg-opacity-40">
{{ $t("energy.fixed_elec_cost") }}
</td>
<td colspan="2" rowspan="2" class="bg-teal-800 bg-opacity-40">
{{ $t("energy.charged_per_household") }}
</td>
<td colspan="2" class="bg-teal-800 bg-opacity-40">
{{ $t("energy.single_phase") }}
</td>
<td rowspan="2" class="bg-teal-800 bg-opacity-40">
{{ $t("energy.per_household_month") }}
</td>
<td colspan="2" class="bg-teal-800 bg-opacity-40">
<input
type="number"
@ -356,7 +434,9 @@ watch(
</td>
</tr>
<tr>
<td colspan="2" class="bg-teal-800 bg-opacity-40">三相</td>
<td colspan="2" class="bg-teal-800 bg-opacity-40">
{{ $t("energy.three_phase") }}
</td>
<td colspan="2" class="bg-teal-800 bg-opacity-40">
<input
type="number"
@ -366,8 +446,12 @@ watch(
</td>
</tr>
<tr>
<td colspan="4" class="bg-teal-800 bg-opacity-40">經常契約</td>
<td rowspan="4" class="bg-teal-800 bg-opacity-40">每瓩每月</td>
<td colspan="4" class="bg-teal-800 bg-opacity-40">
{{ $t("energy.frequent_contract") }}
</td>
<td rowspan="4" class="bg-teal-800 bg-opacity-40">
{{ $t("energy.per_kw_per_month") }}
</td>
<td class="bg-teal-800 bg-opacity-40">
<input
type="number"
@ -384,7 +468,9 @@ watch(
</td>
</tr>
<tr>
<td colspan="4" class="bg-teal-800 bg-opacity-40">非夏日契約</td>
<td colspan="4" class="bg-teal-800 bg-opacity-40">
{{ $t("energy.non_summer_contract") }}
</td>
<td class="bg-teal-800 bg-opacity-40">
<input
type="number"
@ -401,7 +487,9 @@ watch(
</td>
</tr>
<tr>
<td colspan="4" class="bg-teal-800 bg-opacity-40">週六半尖峰契約</td>
<td colspan="4" class="bg-teal-800 bg-opacity-40">
{{ $t("energy.saturday_semi_peak_contract") }}
</td>
<td class="bg-teal-800 bg-opacity-40">
<input
type="number"
@ -418,7 +506,9 @@ watch(
</td>
</tr>
<tr>
<td colspan="4" class="bg-teal-800 bg-opacity-40">離峰契約</td>
<td colspan="4" class="bg-teal-800 bg-opacity-40">
{{ $t("energy.off_peak_contract") }}
</td>
<td class="bg-teal-800 bg-opacity-40">
<input
type="number"
@ -435,12 +525,14 @@ watch(
</td>
</tr>
<tr>
<td rowspan="10">流動電費</td>
<td rowspan="5">週一~週五</td>
<td class="bg-rose-600 bg-opacity-70">尖峰時間</td>
<td class="bg-rose-600 bg-opacity-20">夏月</td>
<td rowspan="10">{{ $t("energy.var_elec_cost") }}</td>
<td rowspan="5">{{ $t("energy.mon_to_friday") }}</td>
<td class="bg-rose-600 bg-opacity-70">{{ $t("energy.peak_hours") }}</td>
<td class="bg-rose-600 bg-opacity-20">
{{ $t("energy.summer_months") }}
</td>
<td class="bg-rose-600 bg-opacity-20">16:00 ~ 22:00</td>
<td rowspan="10">每度</td>
<td rowspan="10">{{ $t("energy.price_per_kwh") }}</td>
<td class="bg-rose-600 bg-opacity-20">
<input
type="number"
@ -451,9 +543,15 @@ watch(
<td class="bg-rose-600 bg-opacity-20">-</td>
</tr>
<tr>
<td rowspan="2" class="bg-yellow-400 bg-opacity-80">半尖峰時間</td>
<td class="bg-yellow-500 bg-opacity-20">夏月</td>
<td class="bg-yellow-500 bg-opacity-20">09:00 ~ 16:00<br />22:00 ~ 24:00</td>
<td rowspan="2" class="bg-yellow-400 bg-opacity-80">
{{ $t("energy.semi_peak_hours") }}
</td>
<td class="bg-yellow-500 bg-opacity-20">
{{ $t("energy.summer_months") }}
</td>
<td class="bg-yellow-500 bg-opacity-20">
09:00 ~ 16:00<br />22:00 ~ 24:00
</td>
<td class="bg-yellow-500 bg-opacity-20">
<input
type="number"
@ -464,8 +562,12 @@ watch(
<td class="bg-yellow-500 bg-opacity-20">-</td>
</tr>
<tr>
<td class="bg-yellow-500 bg-opacity-20">非夏月</td>
<td class="bg-yellow-500 bg-opacity-20">06:00 ~ 11:00<br />14:00 ~ 24:00</td>
<td class="bg-yellow-500 bg-opacity-20">
{{ $t("energy.non_summer_months") }}
</td>
<td class="bg-yellow-500 bg-opacity-20">
06:00 ~ 11:00<br />14:00 ~ 24:00
</td>
<td class="bg-yellow-500 bg-opacity-20">-</td>
<td class="bg-yellow-500 bg-opacity-20">
<input
@ -476,8 +578,12 @@ watch(
</td>
</tr>
<tr>
<td rowspan="2" class="bg-green-600 bg-opacity-60">離峰時間</td>
<td class="bg-green-600 bg-opacity-20">夏月</td>
<td rowspan="2" class="bg-green-600 bg-opacity-60">
{{ $t("energy.off_peak_hours") }}
</td>
<td class="bg-green-600 bg-opacity-20">
{{ $t("energy.summer_months") }}
</td>
<td class="bg-green-600 bg-opacity-20">00:00 ~ 09:00</td>
<td class="bg-green-600 bg-opacity-20">
<input
@ -489,7 +595,9 @@ watch(
<td class="bg-green-600 bg-opacity-20">-</td>
</tr>
<tr>
<td class="bg-green-600 bg-opacity-20">非夏月</td>
<td class="bg-green-600 bg-opacity-20">
{{ $t("energy.non_summer_months") }}
</td>
<td class="bg-green-600 bg-opacity-20">
00:00 ~ 06:00<br />
11:00 ~ 14:00
@ -504,9 +612,13 @@ watch(
</td>
</tr>
<tr>
<td rowspan="4">週六</td>
<td rowspan="2" class="bg-yellow-400 bg-opacity-80">半尖峰時間</td>
<td class="bg-yellow-500 bg-opacity-20">夏日</td>
<td rowspan="4">{{ $t("energy.saturday") }}</td>
<td rowspan="2" class="bg-yellow-400 bg-opacity-80">
{{ $t("energy.semi_peak_hours") }}
</td>
<td class="bg-yellow-500 bg-opacity-20">
{{ $t("energy.summer_months") }}
</td>
<td class="bg-yellow-500 bg-opacity-20">09:00 ~ 24:00</td>
<td class="bg-yellow-500 bg-opacity-20">
<input
@ -518,7 +630,9 @@ watch(
<td class="bg-yellow-500 bg-opacity-20">-</td>
</tr>
<tr>
<td class="bg-yellow-500 bg-opacity-20">非夏日</td>
<td class="bg-yellow-500 bg-opacity-20">
{{ $t("energy.non_summer_months") }}
</td>
<td class="bg-yellow-500 bg-opacity-20">
06:00 ~ 11:00<br />
14:00 ~ 24:00
@ -533,8 +647,12 @@ watch(
</td>
</tr>
<tr>
<td rowspan="2" class="bg-green-600 bg-opacity-60">離峰時間</td>
<td class="bg-green-600 bg-opacity-20">夏月</td>
<td rowspan="2" class="bg-green-600 bg-opacity-60">
{{ $t("energy.off_peak_hours") }}
</td>
<td class="bg-green-600 bg-opacity-20">
{{ $t("energy.summer_months") }}
</td>
<td class="bg-green-600 bg-opacity-20">00:00 ~ 09:00</td>
<td class="bg-green-600 bg-opacity-20">
<input
@ -546,8 +664,12 @@ watch(
<td class="bg-green-600 bg-opacity-20">-</td>
</tr>
<tr>
<td class="bg-green-600 bg-opacity-20">非夏月</td>
<td class="bg-green-600 bg-opacity-20">00:00 ~ 06:00<br />11:00 ~ 14:00</td>
<td class="bg-green-600 bg-opacity-20">
{{ $t("energy.non_summer_months") }}
</td>
<td class="bg-green-600 bg-opacity-20">
00:00 ~ 06:00<br />11:00 ~ 14:00
</td>
<td class="bg-green-600 bg-opacity-20">-</td>
<td class="bg-green-600 bg-opacity-20">
<input
@ -558,9 +680,13 @@ watch(
</td>
</tr>
<tr>
<td>週日及離峰日</td>
<td class="bg-green-600 bg-opacity-60">離峰時間</td>
<td colspan="2" class="bg-green-600 bg-opacity-20">全日</td>
<td>{{ $t("energy.sunday_and_off_peak_days") }}</td>
<td class="bg-green-600 bg-opacity-60">
{{ $t("energy.off_peak_hours") }}
</td>
<td colspan="2" class="bg-green-600 bg-opacity-20">
{{ $t("energy.all_day") }}
</td>
<td class="bg-green-600 bg-opacity-20">
<input
type="number"

View File

@ -1,9 +1,12 @@
<script setup>
import Table from "@/components/customUI/Table.vue";
import FloorsModal from "./FloorsModal.vue";
import { getAssetFloorList, deleteAssetFloor } from "@/apis/asset";
import { deleteAssetFloor } from "@/apis/asset";
import { onMounted, ref, inject, computed } from "vue";
import { useI18n } from "vue-i18n";
import useBuildingStore from "@/stores/useBuildingStore";
const storeBuild = useBuildingStore();
const FILE_BASEURL = import.meta.env.VITE_FILE_API_BASEURL;
const { t } = useI18n();
const { openToast, cancelToastOpen } = inject("app_toast");
@ -28,18 +31,10 @@ const columns = computed(() => [
},
]);
const dataSource = ref([]);
const loading = ref(false);
const getDataSource = async () => {
loading.value = true;
const res = await getAssetFloorList();
dataSource.value = res.data[0]?.floors.map((d) => ({ ...d, key: d.floor_guid }));
loading.value = false;
};
onMounted(() => {
getDataSource();
storeBuild.fetchFloorList(storeBuild.selectedBuilding?.building_guid);
});
const formState = ref({
@ -49,7 +44,20 @@ const formState = ref({
const openModal = (record) => {
if (record.floor_guid) {
formState.value = { ...record };
console.log('record',record);
formState.value.floor_guid = record.floor_guid;
formState.value.full_name = record.full_name;
if(record.floor_map_url){
const floorFile = record
? {
// size: record.oriSize,
name: record.floor_map_name,
src: `${record.floor_map_url}.svg`,
ext: 'svg',
}
: {};
formState.value.floorFile = [floorFile];
}
} else {
formState.value = {
full_name: "",
@ -59,14 +67,14 @@ const openModal = (record) => {
floor_modal.showModal();
};
const remove = async () => {
const remove = async (id) => {
openToast("warning", t("msg.sure_to_delete"), "body", async () => {
await cancelToastOpen();
const res = await deleteAssetFloor({
floor_guid: formState.value.floor_guid,
});
floor_guid: id,
});
if (res.isSuccess) {
getDataSource();
storeBuild.fetchFloorList(storeBuild.selectedBuilding?.building_guid);
openToast("success", t("msg.delete_success"));
} else {
openToast("error", res.msg);
@ -78,13 +86,13 @@ const remove = async () => {
<template>
<div class="flex justify-start items-center mt-10 mb-5">
<h3 class="text-xl mr-5">{{ $t("energy.floor") }}</h3>
<FloorsModal
:formState="formState"
:getData="getDataSource"
:openModal="openModal"
/>
<FloorsModal :formState="formState" :openModal="openModal" />
</div>
<Table :columns="columns" :dataSource="dataSource" :loading="loading">
<Table
:columns="columns"
:dataSource="storeBuild.floorList"
:loading="loading"
>
<template #bodyCell="{ record, column, index }">
<template v-if="column.key === 'index'">{{ index + 1 }}</template>
<template v-else-if="column.key === 'operation'">
@ -96,13 +104,17 @@ const remove = async () => {
</button>
<button
class="btn btn-sm btn-error text-white"
@click.stop.prevent="() => remove()"
@click.stop.prevent="() => remove(record.floor_guid)"
>
{{ $t("button.delete") }}
</button>
</template>
<template v-else-if="column.key === 'floor_plan'">
<img :src="`${FILE_BASEURL}/${record.floor_map_url}.svg`" class="w-[200px] bg-white mx-auto" />
<img
v-if="record.floor_map_url"
:src="`${FILE_BASEURL}/${record.floor_map_url}.svg`"
class="w-[200px] bg-white mx-auto"
/>
</template>
<template v-else>
{{ record[column.key] }}

View File

@ -5,16 +5,17 @@ import "yup-phone-lite";
import useFormErrorMessage from "@/hooks/useFormErrorMessage";
import { postAssetFloor } from "@/apis/asset";
import { useI18n } from "vue-i18n";
import useBuildingStore from "@/stores/useBuildingStore";
const storeBuild = useBuildingStore();
const { t } = useI18n();
const { openToast } = inject("app_toast");
const props = defineProps({
formState: Object,
getData: Function,
openModal: Function,
});
const form = ref(null);
const floorScheme = yup.object({
full_name: yup.string().required(t("button.required")),
floorFile: yup.array(),
@ -22,7 +23,7 @@ const floorScheme = yup.object({
const updateFileList = (files) => {
console.log("file", files);
FloorFormState.value.floorFile = files;
props.formState.floorFile = files;
};
const { formErrorMsg, handleSubmit, handleErrorReset, updateScheme } =
@ -30,23 +31,26 @@ const { formErrorMsg, handleSubmit, handleErrorReset, updateScheme } =
const onCancel = () => {
handleErrorReset();
updateFileList([]);
floor_modal.close();
};
const onOk = async () => {
const value = await handleSubmit(floorScheme, props.formState);
const formData = new FormData(form.value);
formData.append(
"floor_guid",
selectedOption.value === "add" ? null : currentFloor.value.floor_guid
);
formData.append("building_tag", store.selectedBuilding.building_tag);
formData.append("initMapName", FloorFormState.value.floorFile[0]?.name);
formData.append("mapFile", FloorFormState.value.floorFile[0]);
if (props.formState.floor_guid) {
formData.append("floor_guid", props.formState.floor_guid);
}
if (props.formState.floorFile[0]) {
formData.append("initMapName", props.formState.floorFile[0]?.name);
formData.append("mapFile", props.formState.floorFile[0]);
}
formData.append("building_guid", storeBuild.selectedBuilding.building_guid);
formData.delete("floorFile");
const res = await postAssetFloor(formData);
if (res.isSuccess) {
props.getData();
storeBuild.fetchFloorList(storeBuild.selectedBuilding?.building_guid);
onCancel();
} else {
openToast("error", res.msg, "#floor_modal");

View File

@ -79,7 +79,7 @@ const toggleNode = (node, checked) => {
</span>
</div>
<MITTCheckboxTree
<MQTTCheckboxTree
v-if="node.children?.length && node.expanded"
:data="node.children"
:checked-nodes="checkedNodes"

View File

@ -1,16 +1,24 @@
<script setup>
import { ref, onMounted, watch, computed } from "vue";
import { getAssetMainList, getAssetSubList, getIOTSchema } from "@/apis/asset";
import { ref, onMounted, watch, computed, inject } from "vue";
import {
getAssetMainList,
getAssetSubList,
getIOTSchema,
getDeviceItem,
deleteDeviceItem,
} from "@/apis/asset";
import useActiveBtn from "@/hooks/useActiveBtn";
import MQTTListAddModal from "./MQTTListAddModal.vue";
import PointListAddModal from "./PointListAddModal.vue";
import { useI18n } from "vue-i18n";
const { t } = useI18n();
const formState = ref({});
const { openToast, cancelToastOpen } = inject("app_toast");
const MQTTDataSource = ref([]);
const MQTTloading = ref(false);
const PointsDataSource = ref([]);
const Pointsloading = ref(false);
const variable_id = ref(null);
const editRecord = ref(null);
//
const {
items: mainSysItems,
@ -50,36 +58,44 @@ const getSubSystems = async (id) => {
const MQTTColumns = computed(() => [
{
title: "schema",
title: t("setting.schema"),
key: "schema_name",
},
{
title: "point",
title: t("setting.point"),
key: "pointOrg",
filter: true,
},
{
title: "Description",
title: t("setting.description"),
key: "description",
},
]);
const PointsColumns = computed(() => [
{
title: "IoT 點位名稱",
key: "IoT_point_name",
title: t("setting.IoT_point_name"),
key: "full_name",
},
{
title: "小數點個數",
key: "decimal",
title: t("setting.IoT_point_code"),
key: "points",
},
{
title: "bool 值",
title: t("setting.number_of_decimal_places"),
key: "decimals",
},
{
title: t("setting.boolean_value"),
key: "is_bool",
},
{
title: "點位隱藏",
key: "is_open",
title: t("setting.hide_point"),
key: "is_link",
},
{
title: t("assetManagement.operation"),
key: "operation",
},
]);
@ -97,6 +113,46 @@ const getDataSource = async (id) => {
MQTTloading.value = false;
};
const getPointsData = async (id) => {
Pointsloading.value = true;
const res = await getDeviceItem(id);
PointsDataSource.value = res.data.map((d) => ({
...d,
key: d.id,
}));
Pointsloading.value = false;
};
const remove = async (id) => {
openToast("warning", t("msg.sure_to_delete"), "body", async () => {
await cancelToastOpen();
const res = await deleteDeviceItem(id);
if (res.isSuccess) {
getPointsData(variable_id.value);
openToast("success", t("msg.delete_success"));
} else {
openToast("error", res.msg);
}
});
};
const openModal = (record) => {
if (record?.id) {
editRecord.value = record;
} else {
editRecord.value = {
id: 0,
variable_id: variable_id.value,
full_name: "",
points: "",
decimals: 0,
is_bool: 0,
is_link: 0,
};
}
point_list_item.showModal();
};
watch(
selectedMainSysItems,
(newValue) => {
@ -114,6 +170,8 @@ watch(
(newValue) => {
if (newValue && newValue.key) {
getDataSource(parseInt(newValue.key));
getPointsData(parseInt(newValue.key));
variable_id.value = parseInt(newValue.key);
}
},
{
@ -157,23 +215,55 @@ onMounted(() => {
/>
</div>
<div class="flex justify-start items-center mt-5 mb-2">
<h3 class="text-xl mr-5">MQTT_Parse : </h3>
<MQTTListAddModal />
<h3 class="text-xl mr-5">{{ $t("setting.MQTT_parse") }} :</h3>
<MQTTListAddModal
:getData="getDataSource"
:pointsData="PointsDataSource"
:variable_id="variable_id"
/>
</div>
<Table
:columns="MQTTColumns"
:dataSource="MQTTDataSource"
:loading="MQTTloading"
></Table>
>
</Table>
<div class="flex justify-start items-center mt-5 mb-2">
<h3 class="text-xl mr-5">Points : </h3>
<PointListAddModal />
<h3 class="text-xl mr-5">{{ $t("setting.point") }} :</h3>
<PointListAddModal
:getData="getPointsData"
:variable_id="variable_id"
:editRecord="editRecord"
:openModal="openModal"
/>
</div>
<Table
:columns="PointsColumns"
:dataSource="PointsDataSource"
:loading="Pointsloading"
></Table>
><template #bodyCell="{ record, column, index }">
<template v-if="column.key === 'is_bool'">
{{ record.is_bool === 1 ? t("alert.yes") : t("alert.no") }}
</template>
<template v-else-if="column.key === 'is_link'">
{{ record.is_link === 1 ? t("alert.yes") : t("alert.no") }}
</template>
<template v-else-if="column.key === 'operation'">
<button
class="btn btn-sm btn-success text-white mr-2"
@click.stop.prevent="() => openModal(record)"
>
{{ $t("button.edit") }}
</button>
<button
class="btn btn-sm btn-error text-white"
@click.stop.prevent="() => remove(record.id)"
>
{{ $t("button.delete") }}
</button>
</template>
</template>
</Table>
</div>
</template>

View File

@ -1,10 +1,15 @@
<script setup>
import { ref, onMounted, watch, defineProps } from "vue";
import { getAssetMainList, getAssetSubList, getIOTSchema } from "@/apis/asset";
import useActiveBtn from "@/hooks/useActiveBtn";
import { ref, onMounted, watch, defineProps, inject } from "vue";
import { postIOTSchema } from "@/apis/asset";
import MQTTCheckboxTree from "./MQTTCheckboxTree.vue";
import generateSchema from "json-schema-generator";
import { useI18n } from "vue-i18n";
const { openToast } = inject("app_toast");
const props = defineProps({
getData: Function,
pointsData: Object,
variable_id: Number,
});
const { t } = useI18n();
// jsonInput
const jsonInput = ref("");
@ -14,10 +19,6 @@ const checkedNodes = ref([]);
const form = ref(null);
const schemaName = ref("");
const formStates = ref([]);
const optionData = ref([
{ key: 0, full_name: "否" },
{ key: 1, full_name: "是" },
]);
// JSON Schema
const customGenerateSchema = (json) => {
@ -92,22 +93,42 @@ watch(
(newCheckedNodes) => {
// node formState
formStates.value = newCheckedNodes.map((node) => ({
IoT_point_schema: node,
sys_point_name: null,
is_bool: 1,
decimal: 0,
is_open: 1,
PointOrg: node,
PointSys: "",
item_id: null,
}));
},
{ immediate: true }
);
const onOk = async () => {
const points = formStates.value.map((state) => ({
PointOrg: state.PointOrg,
PointSys: props.pointsData.find((point) => point.id === state.item_id)
.points,
item_id: state.item_id,
}));
const apiData = {
name: schemaName.value,
variable_id: props.variable_id,
points: points,
};
const res = await postIOTSchema(apiData);
if (res.isSuccess) {
props.getData(props.variable_id);
onCancel();
} else {
openToast("error", res.msg, "#MQTT_Parse_item");
}
};
const openModal = () => {
MQTT_Parse_item.showModal();
};
const onCancel = () => {
MQTT_Parse_item.close();
clearAll();
};
</script>
@ -117,7 +138,7 @@ const onCancel = () => {
</button>
<Modal
id="MQTT_Parse_item"
title="MQTT_Parse"
:title="t('setting.MQTT_parse')"
:onCancel="onCancel"
:width="1600"
>
@ -126,21 +147,18 @@ const onCancel = () => {
<div class="w-full">
<textarea
v-model="jsonInput"
placeholder="請貼上 JSON 格式數據"
:placeholder="t('setting.json_format_text')"
class="w-full h-[500px] p-4 border rounded-lg font-mono text-white"
></textarea>
</div>
<div class="my-auto">
<button @click="parseJson" class="btn-success btn w-24 mb-4">
轉換
<font-awesome-icon
:icon="['fas', 'chevron-right']"
size="lg"
class="block"
/>
<button @click="parseJson" class="btn-success btn w-28 mb-4">
{{ $t("button.convert")
}}<font-awesome-icon :icon="['fas', 'chevron-right']" />
</button>
<button @click="clearAll" class="bg-error btn w-24">
刪除<font-awesome-icon :icon="['fas', 'trash-alt']" />
<button @click="clearAll" class="bg-error btn w-28">
{{ $t("button.delete")
}}<font-awesome-icon :icon="['fas', 'trash-alt']" />
</button>
</div>
<div class="w-full p-4 rounded-lg border overflow-y-scroll h-[500px]">
@ -149,81 +167,71 @@ const onCancel = () => {
:data="treeData"
v-model:checked-nodes="checkedNodes"
/>
<div v-else class="text-white">請在左側輸入 JSON 並點擊轉換按鈕</div>
<div v-else class="text-white">
{{ $t("setting.json_click_text") }}
</div>
</div>
</div>
<form ref="form" class="" v-if="checkedNodes.length">
<div class="flex items-center mt-5">
<h2 class="text-lg font-bold whitespace-nowrap me-2">結構名稱 :</h2>
<Input v-model="schemaName" />
<h2 class="text-lg font-bold whitespace-nowrap me-2">
{{ $t("setting.schema_name") }}:
</h2>
<input
type="text"
v-model.text="schemaName"
class="input border-info focus-within:border-info"
/>
</div>
<table class="table">
<table class="table w-1/2 mt-2">
<thead>
<tr>
<!-- <th>IoT 點位名稱</th> -->
<th>IoT 點位結構</th>
<th>系統點位名稱</th>
<!-- <th>bool </th>
<th>小數點個數</th>
<th>點位隱藏</th> -->
<th>{{$t("setting.IoT_point_structure")}}</th>
<th>{{$t("setting.system_point_name")}}</th>
</tr>
</thead>
<tbody>
<tr v-for="(node, index) in checkedNodes" :key="node">
<!-- <td>
<Input :value="formStates[index]" name="IoT_point_name" />
</td> -->
<td class="w-1/2">
<td>
<Input
:value="formStates[index]"
name="IoT_point_schema"
name="PointOrg"
:readonly="true"
class="mx-auto"
/>
</td>
<td>
<select>
<option value="option1">選項 1</option>
<option value="option2">選項 2</option>
<option value="option3">選項 3</option>
</select>
</td>
<!-- <td>
<div class="flex w-36">
<Select
:value="formStates[index]"
selectClass="w-20 mx-auto border-info focus-within:border-info"
name="is_bool"
Attribute="full_name"
:options="optionData"
>
</Select>
</div>
</td>
<td>
<InputNumber
<Select
:value="formStates[index]"
class="mx-auto"
name="decimal"
class="my-2 mx-auto"
selectClass="border-info focus-within:border-info "
name="item_id"
Attribute="full_name"
:options="props.pointsData"
>
</InputNumber>
</Select>
</td>
<td>
<div class="flex w-36">
<Select
:value="formStates[index]"
selectClass="w-20 mx-auto border-info focus-within:border-info"
name="is_open"
Attribute="full_name"
:options="optionData"
>
</Select>
</div>
</td> -->
</tr>
</tbody>
</table>
</form>
</template>
<template #modalAction>
<button
type="reset"
class="btn btn-outline-success mr-2"
@click.prevent="onCancel"
>
{{ $t("button.cancel") }}
</button>
<button
type="submit"
class="btn btn-outline-success"
@click.stop.prevent="onOk"
>
{{ $t("button.submit") }}
</button>
</template>
</Modal>
</template>

View File

@ -1,56 +1,114 @@
<script setup>
import { ref, onMounted, watch, defineProps } from "vue";
import { ref, onMounted, watch, defineProps, inject, readonly } from "vue";
import useFormErrorMessage from "@/hooks/useFormErrorMessage";
import { postDeviceItem } from "@/apis/asset";
import { useI18n } from "vue-i18n";
import * as yup from "yup";
const { openToast } = inject("app_toast");
const { t } = useI18n();
const props = defineProps({
getData: Function,
openModal: Function,
editRecord: Object,
variable_id: Number,
});
const form = ref(null);
const formState = ref({
id: 0,
IoT_point_name: "",
variable_id: props.variable_id,
full_name: "",
points: "",
decimals: 0,
is_bool: 0,
decimal: 2,
is_open: 0,
is_link: 0,
});
let schema = ref(
yup.object({
IoT_point_name: yup.string().required(t("button.required")),
is_bool: yup.string().required(t("button.required")),
decimal: yup.string().required(t("button.required")),
is_open: yup.string().required(t("button.required")),
full_name: yup.string().required(t("button.required")),
decimals: yup.number().required(t("button.required")),
is_bool: yup.number().required(t("button.required")),
is_link: yup.number().required(t("button.required")),
})
);
const { formErrorMsg, handleSubmit, handleErrorReset, updateScheme } =
useFormErrorMessage(schema.value);
const openModal = () => {
point_list_item.showModal();
const onOk = async () => {
const values = await handleSubmit(schema.value, formState.value);
console.log("value", values);
const res = await postDeviceItem({
...values,
decimals: Number(values.decimals),
is_bool: Number(values.is_bool),
is_link: Number(values.is_link),
});
if (res.isSuccess) {
props.getData(props.variable_id);
onCancel();
} else {
openToast("error", res.msg, "#point_list_item");
}
};
const onCancel = () => {
point_list_item.close();
};
watch(
() => props.editRecord,
(newValue) => {
if (newValue) {
formState.value = {
...newValue,
};
} else {
formState.value = {
id: 0,
variable_id: props.variable_id,
full_name: "",
points: "",
decimals: 0,
is_bool: 0,
is_link: 0,
};
}
}
);
</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="props.openModal">
<font-awesome-icon :icon="['fas', 'plus']" />{{ $t("button.add") }}
</button>
<Modal id="point_list_item" title="Points" :onCancel="onCancel" :width="710">
<template #modalContent>
<form ref="form" class="mt-5 w-full flex flex-wrap justify-between">
<Input :value="formState" class="my-2" name="IoT_point_name">
<template #topLeft>IoT 點位名稱</template>
<Input :value="formState" class="my-2" name="full_name">
<template #topLeft>{{ $t("setting.IoT_point_name") }}</template>
<template #bottomLeft
><span class="text-error text-base">
{{ formErrorMsg.IoT_point_name }}
{{ formErrorMsg.full_name }}
</span></template
></Input
>
<InputNumber :value="formState" class="my-2" name="decimal">
<template #topLeft>小數點個數</template>
<Input
:value="formState"
class="my-2"
name="points"
:disabled="props.editRecord?.id"
>
<template #topLeft>{{ $t("setting.IoT_point_code") }}</template>
<template #bottomLeft
><span class="text-error text-base">
{{ formErrorMsg.points }}
</span></template
></Input
>
<InputNumber :value="formState" class="my-2" name="decimals">
<template #topLeft>{{
$t("setting.number_of_decimal_places")
}}</template>
</InputNumber>
<RadioGroup
class="my-2"
@ -70,12 +128,12 @@ const onCancel = () => {
]"
:required="true"
>
<template #topLeft>bool </template>
<template #topLeft>{{ $t("setting.boolean_value") }}</template>
</RadioGroup>
<RadioGroup
class="my-2"
name="is_open"
name="is_link"
:value="formState"
:items="[
{
@ -91,10 +149,26 @@ const onCancel = () => {
]"
:required="true"
>
<template #topLeft>點位隱藏</template>
<template #topLeft>{{ $t("setting.hide_point") }}</template>
</RadioGroup>
</form>
</template>
<template #modalAction>
<button
type="reset"
class="btn btn-outline-success mr-2"
@click.prevent="onCancel"
>
{{ $t("button.cancel") }}
</button>
<button
type="submit"
class="btn btn-outline-success"
@click.stop.prevent="onOk"
>
{{ $t("button.submit") }}
</button>
</template>
</Modal>
</template>

View File

@ -93,7 +93,7 @@ watch(
},
params.data[2]
);
selected_dbid.value[0] = params.data[2].forge_dbid;
selected_dbid.value[1] = params.data[2].spriteDbId;
});
}
},
@ -146,7 +146,7 @@ watch(
allData.value
) {
selectedData.value = selectedData.value.map((item) => {
if (item[2].forge_dbid === newSelectedDbid[0]) {
if (item[2].spriteDbId === newSelectedDbid[1]) {
return [...item.slice(0, 2), { ...item[2], is2DActive: true }];
}
return [...item.slice(0, 2), { ...item[2], is2DActive: false }];

View File

@ -1,51 +1,118 @@
<script setup>
import { computed, inject, watch } from "vue"
import useSystemShowData from "@/hooks/useSystemShowData"
import { computed, inject, watch } from "vue";
import useSystemShowData from "@/hooks/useSystemShowData";
const { getCurrentInfoModalData, selected_dbid } = inject("system_selectedDevice")
const { getCurrentInfoModalData, selected_dbid } = inject(
"system_selectedDevice"
);
const { subscribeData } = inject("system_deviceList");
const { showData } = useSystemShowData()
const { showData } = useSystemShowData();
const FILE_BASEURL = import.meta.env.VITE_FILE_API_BASEURL;
const fitToView = (forge_dbid, spriteDbId) => {
selected_dbid.value = [forge_dbid, spriteDbId];
};
const openDialog = () => {
document.getElementById("iframe_modal").showModal();
};
const cancelDialog = () => {
document.getElementById("iframe_modal").close();
};
</script>
<template>
<!-- <InfoModal :data="currentInfoModalData" /> -->
<dialog
id="iframe_modal"
className="modal w-[590px] h-[490px] my-auto rounded-lg mx-auto"
>
<iframe
src="https://app.mockplus.cn/p/zNB4y8k5L"
frameborder="0"
class="w-full h-full"
></iframe>
<Button
type="link"
class="btn-link btn-text-without-border z-20 absolute right-[2.3rem] top-[2.2rem]"
@click="cancelDialog"
>
<font-awesome-icon
:icon="['fas', 'times']"
class="text-[#a5abb1] text-2xl"
/>
</Button>
</dialog>
<template v-if="showData.length > 0">
<div class="equipment-show" v-for="d in showData" :key="d.full_name">
<template v-if="d.device_list.length > 0">
<p class="title">{{ d.full_name }}</p>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-5">
<div class="col-auto relative" v-for="device in d.device_list" :key="device.device_guid">
<div class="item h-full cursor-pointer" @click="() => fitToView(device.forge_dbid, device.spriteDbId)">
<div
class="col-auto relative"
v-for="device in d.device_list"
:key="device.device_guid"
>
<div
class="item h-full cursor-pointer"
@click="() => fitToView(device.forge_dbid, device.spriteDbId)"
>
<div class="left h-full flex flex-wrap justify-center">
<div class="sec02 w-full">
<img v-if="device.device_image_url" :src="`${FILE_BASEURL}/upload/device_icon/${device.device_image}`"
:alt="device.device_image">
<img
v-if="device.device_image_url"
:src="`${FILE_BASEURL}/upload/device_icon/${device.device_image}`"
:alt="device.device_image"
/>
<span v-else></span>
<span>{{ device.full_name }}</span>
</div>
<div class="flex justify-between w-full self-end">
<div class="sec03">
<span class="w-5 h-5 rounded-full" :style="{ backgroundColor: device.device_normal_color }"></span>
<span
class="w-5 h-5 rounded-full"
:style="{
backgroundColor:
device.full_name === 'SmartSocket-AA001'
? 'red'
: device.full_name === 'SmartSocket-AA003' ||
device.full_name === 'SmartSocket-AA004'
? 'gray'
: device.device_normal_color,
}"
></span>
<span class="mx-2">{{ $t("system.status") }}:</span>
<span>{{ device.device_status }}</span>
<span>{{
device.full_name === "SmartSocket-AA001"
? "Error"
: device.full_name === "SmartSocket-AA003" ||
device.full_name === "SmartSocket-AA004"
? "Offline"
: device.device_status || 'Online'
}}</span>
</div>
<button class="btn-text border-0 "
@click.prevent="(e) => getCurrentInfoModalData(e, { left: e.clientX, top: e.clientY }, device)">{{
$t("system.details") }}</button>
<button
class="btn-text border-0"
@click.prevent="
(e) => {
if (device.full_name === 'SmartSocket-AA001') {
openDialog();
} else {
getCurrentInfoModalData(
e,
{ left: e.clientX, top: e.clientY },
device
);
}
}
"
>
{{ $t("system.details") }}
</button>
</div>
</div>
</div>
</div>
</div>
</template>
@ -53,7 +120,7 @@ const fitToView = (forge_dbid, spriteDbId) => {
</template>
</template>
<style lang='css' scoped>
<style lang="css" scoped>
/*設備顯示*/
.title {
@apply text-lg text-white relative inline-block mb-5 after:absolute after:-bottom-2 after:left-1/4 after:w-4/5 after:h-[1px] after:bg-info;
@ -88,7 +155,7 @@ const fitToView = (forge_dbid, spriteDbId) => {
}
.equipment-show .item .sec01 span:nth-child(2) {
font-size: .6rem;
font-size: 0.6rem;
}
.equipment-show .item .sec02 {

View File

@ -12,18 +12,24 @@ const {
changeActiveBtn: changeDeptActiveBtn,
setItems: setDeptItems,
selectedBtn: selectedDeptItems,
} = useActiveBtn();
} = useActiveBtn("multiple");
onMounted(() => {
const deptList = [
{
title: "All",
key: "main",
},
...storeBuild.deptList,
];
setDeptItems(deptList);
});
watch(
() => storeBuild.deptList,
(newValue) => {
if (newValue) {
const deptList = newValue.map((d) => ({
...d,
active: true,
}));
setDeptItems(deptList);
}
},
{
deep: true,
immediate: true,
}
);
</script>
<template>
@ -34,6 +40,7 @@ onMounted(() => {
<ButtonGroup
:items="sysDeptItems"
className="btn-xs rounded-md"
:withLine="true"
:onclick="
(e, item) => {
changeDeptActiveBtn(item);

View File

@ -1,74 +1,88 @@
<script setup>
import { useRoute, useRouter } from 'vue-router';
import { getAssetFloorList } from "@/apis/asset";
import { onMounted, ref, watch, inject } from 'vue';
import { useRoute, useRouter } from "vue-router";
import { onMounted, ref, watch, inject } from "vue";
import useBuildingStore from "@/stores/useBuildingStore";
import useActiveBtn from "@/hooks/useActiveBtn"
import useActiveBtn from "@/hooks/useActiveBtn";
const route = useRoute()
const router = useRouter()
const route = useRoute();
const router = useRouter();
const store = useBuildingStore();
const { updateCurrentFloor } = inject("system_deviceList")
const { updateCurrentFloor } = inject("system_deviceList");
const { items, changeActiveBtn, setItems, selectedBtn } = useActiveBtn();
const getFloors = async () => {
const res = await getAssetFloorList()
let data = res.data.find(d => d.building_tag === store.selectedBuilding?.building_tag)
data = [
{
title: "All",
key: "main",
active: route.params.floor_id === "main",
},
...data.floors.map((d, idx) => ({
title: d.full_name,
key: d.floor_guid,
active: route.params.floor_id === d.floor_guid,
map_url: d.floor_map_url ? d.floor_map_url + ".svg" : null
}))
]
setItems(data);
updateCurrentFloor(data)
}
const onClick = (item) => {
changeActiveBtn(item)
changeActiveBtn(item);
router.push({
name: 'sub_system', params: {
...route.params, floor_id: item.key
}, query: { ...route.query, gas: route.query.gas, mode: item.key === "main" ? "3D" : route.query.mode }
})
name: "sub_system",
params: {
...route.params,
floor_id: item.key,
},
query: {
...route.query,
gas: route.query.gas,
mode: item.key === "main" ? "3D" : route.query.mode,
},
});
};
}
watch(() => route.params.sub_system_id, (newValue, oldValue) => {
if (newValue !== oldValue) {
setItems(items.value.map((item, index) => ({ ...item, active: index === 0 })));
watch(
() => route.params.sub_system_id,
(newValue, oldValue) => {
if (newValue !== oldValue) {
setItems(
items.value.map((item, index) => ({ ...item, active: index === 0 }))
);
}
},
{
deep: true,
}
);
}, {
deep: true
})
watch(() => store.selectedBuilding, (newValue) => {
newValue && getFloors()
}, {
deep: true,
immediate: true
})
watch(
() => store.floorList,
(newValue) => {
if (newValue) {
console.log('newValue',newValue);
const floorList = [
{
title: "All",
key: "main",
active: route.params.floor_id === "main",
},
...store.floorList.map((d, idx) => ({
title: d.full_name,
key: d.floor_guid,
active: route.params.floor_id === d.floor_guid,
map_url: d.floor_map_url ? d.floor_map_url + ".svg" : null,
})),
];
setItems(floorList);
updateCurrentFloor(floorList);
}
},
{
deep: true,
immediate: true,
}
);
</script>
<template>
<div class="flex items-center gap-3">
<span class="text-md font-extrabold">{{ $t("energy.floor") }} :</span>
<ButtonGroup :items="items" :withLine="false" className="btn-xs rounded-md" :onclick="(e, item) => onClick(item)" />
<ButtonGroup
:items="items"
:withLine="true"
className="btn-xs rounded-md"
:onclick="(e, item) => onClick(item)"
/>
</div>
</template>
<style lang='scss' scoped></style>
<style lang="scss" scoped></style>