Compare commits
11 Commits
main
...
feature/he
Author | SHA1 | Date | |
---|---|---|---|
64f35db51b | |||
2dfc2e5297 | |||
8243900f96 | |||
5ba844e307 | |||
3976540280 | |||
3427058cd2 | |||
e05e83bb03 | |||
73a76aca2e | |||
db5f15dfde | |||
1812ce2495 | |||
8d23b695c6 |
@ -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://mqttwss.mjm-staging.developers-homelab.net"
|
||||
# VITE_MQTT_BASEURL = "wss://mqttwss.mjm-staging.developers-homelab.net"
|
||||
VITE_FORGE_BASEURL = "https://cgems.cvilux-group.com:8088/dist"
|
@ -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://mqttwss.mjm-staging.developers-homelab.net"
|
||||
# VITE_MQTT_BASEURL = "wss://mqttwss.mjm-staging.developers-homelab.net"
|
||||
# VITE_FORGE_BASEURL = "https://cgems.cvilux-group.com:8088/dist"
|
@ -1,3 +1,3 @@
|
||||
VITE_API_BASEURL = "https://ibms-cvilux-demo-api.production.mjmtech.com.tw"
|
||||
VITE_FILE_API_BASEURL = "https://cgems.cvilux-group.com:8088"
|
||||
VITE_MQTT_BASEURL = "wss://mqttwss.mjm-staging.developers-homelab.net"
|
||||
# VITE_MQTT_BASEURL = "wss://mqttwss.mjm-staging.developers-homelab.net"
|
7
package-lock.json
generated
7
package-lock.json
generated
@ -22,6 +22,7 @@
|
||||
"echarts": "^5.4.3",
|
||||
"jquery-ui": "^1.14.1",
|
||||
"json-schema-generator": "^2.0.6",
|
||||
"leaflet": "^1.9.4",
|
||||
"mqtt": "^5.10.3",
|
||||
"pinia": "^2.1.7",
|
||||
"requirejs": "^2.3.6",
|
||||
@ -3350,6 +3351,12 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/leaflet": {
|
||||
"version": "1.9.4",
|
||||
"resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.4.tgz",
|
||||
"integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==",
|
||||
"license": "BSD-2-Clause"
|
||||
},
|
||||
"node_modules/libphonenumber-js": {
|
||||
"version": "1.10.60",
|
||||
"resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.10.60.tgz",
|
||||
|
@ -24,6 +24,7 @@
|
||||
"echarts": "^5.4.3",
|
||||
"jquery-ui": "^1.14.1",
|
||||
"json-schema-generator": "^2.0.6",
|
||||
"leaflet": "^1.9.4",
|
||||
"mqtt": "^5.10.3",
|
||||
"pinia": "^2.1.7",
|
||||
"requirejs": "^2.3.6",
|
||||
|
BIN
public/CviLux_globalmap.jpg
Normal file
BIN
public/CviLux_globalmap.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 42 KiB |
BIN
public/CviLux_globalmap.png
Normal file
BIN
public/CviLux_globalmap.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 552 KiB |
@ -37,3 +37,5 @@ export const DELETE_ASSET_ELECTYPE_API = `/AssetManage/DeleteElecType`;
|
||||
export const POST_ASSET_ELEC_SETTING_API = `/AssetManage/SaveAssetSetting`;
|
||||
|
||||
export const POST_ASSET_MQTT_PUBLISH_API = `/api/mqtt/publish`;
|
||||
export const POST_MQTT_TOPIC_API = `api/Device/MQTTTopicTest`;
|
||||
export const POST_MQTT_TOPIC_STOP_API = `api/Device/MQTTTopicTestStop`;
|
@ -27,6 +27,8 @@ import {
|
||||
DELETE_ASSET_ELECTYPE_API,
|
||||
POST_ASSET_ELEC_SETTING_API,
|
||||
POST_ASSET_MQTT_PUBLISH_API,
|
||||
POST_MQTT_TOPIC_API,
|
||||
POST_MQTT_TOPIC_STOP_API,
|
||||
} from "./api";
|
||||
import instance from "@/util/request";
|
||||
import apihandler from "@/util/apihandler";
|
||||
@ -359,3 +361,21 @@ export const postMQTTpublish = async ({ Topic, Payload }) => {
|
||||
code: res.code,
|
||||
});
|
||||
};
|
||||
|
||||
export const postMqttTopic = async ({ iotTag, Topic }) => {
|
||||
const res = await instance.post(POST_MQTT_TOPIC_API, { iotTag, Topic });
|
||||
|
||||
return apihandler(res.code, res.data, {
|
||||
msg: res.msg,
|
||||
code: res.code,
|
||||
});
|
||||
};
|
||||
|
||||
export const postMqttTopicStop = async ({ iotTag, Topic }) => {
|
||||
const res = await instance.post(POST_MQTT_TOPIC_STOP_API, { iotTag, Topic });
|
||||
|
||||
return apihandler(res.code, res.data, {
|
||||
msg: res.msg,
|
||||
code: res.code,
|
||||
});
|
||||
};
|
||||
|
@ -4,3 +4,4 @@ export const DELETE_BUILDING_API = `/AssetManage/DeleteBuilding`;
|
||||
export const GET_AUTHPAGE_API = `/api/GetUsrFroList`;
|
||||
export const GET_SUBAUTHPAGE_API = `/api/Device/GetMainSub`;
|
||||
export const GET_ALL_DEVICE_API = `/api/Device/GetAllDevice`;
|
||||
export const GET_FUNCTION_LIST_API = `/api/function/get-function-list`;
|
||||
|
@ -5,6 +5,7 @@ import {
|
||||
GET_AUTHPAGE_API,
|
||||
GET_SUBAUTHPAGE_API,
|
||||
GET_ALL_DEVICE_API,
|
||||
GET_FUNCTION_LIST_API,
|
||||
} from "./api";
|
||||
import instance from "@/util/request";
|
||||
import apihandler from "@/util/apihandler";
|
||||
@ -39,9 +40,9 @@ export const deleteBuildings = async (building_guid) => {
|
||||
});
|
||||
};
|
||||
|
||||
export const getAuth = async (lang) => {
|
||||
const res = await instance.post(GET_AUTHPAGE_API, {
|
||||
lang,
|
||||
export const getAuth = async (building_id) => {
|
||||
const res = await instance.get(GET_FUNCTION_LIST_API, {
|
||||
params: { building_id },
|
||||
});
|
||||
return apihandler(res.code, res.data, {
|
||||
msg: res.msg,
|
||||
|
@ -1,6 +1,6 @@
|
||||
export const GET_REALTIME_DIST_API = `/api/Energe/GetRealTimeDistribution`;
|
||||
export const GET_ELECUSE_DAY_API = `/api/Energe/GetElecUseDay`;
|
||||
export const GET_TAI_POWER_API = `/api/Energe/GetTaipower`;
|
||||
export const GET_TAI_POWER_API = `/api/energy-manager/power-usage`;
|
||||
|
||||
export const GET_SIDEBAR_API = `/api/GetSideBar`;
|
||||
export const GET_SEARCH_API = `/api/Energe/GetFilter`;
|
||||
|
8
src/apis/headquarters/api.js
Normal file
8
src/apis/headquarters/api.js
Normal file
@ -0,0 +1,8 @@
|
||||
export const GET_SITES_SYSTEM_STATUS_API = `/api/monitoring/sites-system-status`;
|
||||
export const GET_SITES_SYSTEM_ENERGY_COST_RANK_API = `/api/energy-manager/all-site/energy-cost-rank`;
|
||||
export const GET_SITES_SYSTEM_ENERGY_COST_TREND_API = `/api/energy-manager/all-site/energy-cost-trend`;
|
||||
export const GET_SITES_SYSTEM_ENERGY_COST_GROWTH_API = `/api/energy-manager/all-site/energy-cost-growth-rate`;
|
||||
|
||||
export const GET_USER_API = `/api/user/user-list`;
|
||||
|
||||
|
64
src/apis/headquarters/index.js
Normal file
64
src/apis/headquarters/index.js
Normal file
@ -0,0 +1,64 @@
|
||||
import {
|
||||
GET_SITES_SYSTEM_STATUS_API,
|
||||
GET_SITES_SYSTEM_ENERGY_COST_RANK_API,
|
||||
GET_SITES_SYSTEM_ENERGY_COST_TREND_API,
|
||||
GET_SITES_SYSTEM_ENERGY_COST_GROWTH_API,
|
||||
GET_USER_API,
|
||||
} from "./api";
|
||||
import instance from "@/util/request";
|
||||
import apihandler from "@/util/apihandler";
|
||||
|
||||
export const getSystemStatus = async (building_ids) => {
|
||||
const res = await instance.post(GET_SITES_SYSTEM_STATUS_API, building_ids);
|
||||
|
||||
return apihandler(res.code, res.data, {
|
||||
msg: res.msg,
|
||||
code: res.code,
|
||||
});
|
||||
};
|
||||
|
||||
export const getSystemEnergyCostRank = async (building_ids) => {
|
||||
const res = await instance.post(GET_SITES_SYSTEM_ENERGY_COST_RANK_API, building_ids);
|
||||
|
||||
return apihandler(res.code, res.data, {
|
||||
msg: res.msg,
|
||||
code: res.code,
|
||||
});
|
||||
};
|
||||
|
||||
export const getSystemEnergyCostTrend = async (building_ids) => {
|
||||
const res = await instance.post(GET_SITES_SYSTEM_ENERGY_COST_TREND_API, building_ids);
|
||||
|
||||
return apihandler(res.code, res.data, {
|
||||
msg: res.msg,
|
||||
code: res.code,
|
||||
});
|
||||
}
|
||||
|
||||
export const getSystemEnergyCostGrowth = async (building_ids) => {
|
||||
const res = await instance.get(GET_SITES_SYSTEM_ENERGY_COST_GROWTH_API, building_ids);
|
||||
|
||||
return apihandler(res.code, res.data, {
|
||||
msg: res.msg,
|
||||
code: res.code,
|
||||
});
|
||||
}
|
||||
|
||||
export const getUserList = async (params = {}) => {
|
||||
const {
|
||||
page = 1,
|
||||
pageSize = 9999999
|
||||
} = params;
|
||||
|
||||
const requestData = {
|
||||
Page: page,
|
||||
PageSize: pageSize
|
||||
};
|
||||
|
||||
const res = await instance.post(GET_USER_API, requestData);
|
||||
|
||||
return apihandler(res.code, res.data, {
|
||||
msg: res.msg,
|
||||
code: res.code,
|
||||
});
|
||||
}
|
@ -1,3 +1,4 @@
|
||||
export const GET_SYSTEM_FLOOR_LIST_API = `/api/Device/GetFloor`;
|
||||
export const GET_SYSTEM_DEVICE_LIST_API = `/api/Device/GetDeviceList`;
|
||||
export const GET_SYSTEM_REALTIME_API = `/api/Device/GetRealTimeData`;
|
||||
export const GET_SYSTEM_DEVICE_POWER_TOGGLE_API = `/api/device-events/power-toggle`;
|
@ -2,6 +2,7 @@ import {
|
||||
GET_SYSTEM_FLOOR_LIST_API,
|
||||
GET_SYSTEM_DEVICE_LIST_API,
|
||||
GET_SYSTEM_REALTIME_API,
|
||||
GET_SYSTEM_DEVICE_POWER_TOGGLE_API
|
||||
} from "./api";
|
||||
import instance from "@/util/request";
|
||||
import apihandler from "@/util/apihandler";
|
||||
@ -42,3 +43,16 @@ export const getSystemRealTime = async (device_list) => {
|
||||
code: res.code,
|
||||
});
|
||||
};
|
||||
|
||||
export const toggleDevicePower = async ({topic_publish, device_item_id,new_value}) => {
|
||||
const res = await instance.post(GET_SYSTEM_DEVICE_POWER_TOGGLE_API, {
|
||||
topic_publish,
|
||||
device_item_id,
|
||||
new_value,
|
||||
});
|
||||
|
||||
return apihandler(res.code, res.data, {
|
||||
msg: res.msg,
|
||||
code: res.code,
|
||||
});
|
||||
}
|
||||
|
@ -1,11 +1,18 @@
|
||||
<script setup>
|
||||
import { onMounted } from "vue";
|
||||
import { onMounted,watch } from "vue";
|
||||
import useBuildingStore from "@/stores/useBuildingStore";
|
||||
import { useRouter } from "vue-router";
|
||||
|
||||
const store = useBuildingStore();
|
||||
|
||||
const router = useRouter();
|
||||
const selectBuilding = (bui) => {
|
||||
store.selectedBuilding = bui; // 改變 selectedBuilding,watch 會自動更新資料
|
||||
|
||||
if (bui.is_headquarter == true) {
|
||||
router.replace({ path: "/headquarters" });
|
||||
} else {
|
||||
router.replace({ path: "/dashboard" });
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
|
@ -9,7 +9,7 @@ import { useI18n } from "vue-i18n";
|
||||
import { useRoute } from "vue-router";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
const { locale } = useI18n();
|
||||
const { locale, t } = useI18n();
|
||||
const store = useUserInfoStore();
|
||||
const buildingStore = useBuildingStore();
|
||||
const route = useRoute();
|
||||
@ -17,8 +17,8 @@ const openKeys = ref([]); // 追蹤當前打開的子菜單
|
||||
const menu_array = ref([]);
|
||||
const currentAuthCode = ref("");
|
||||
|
||||
const iniFroList = async () => {
|
||||
const res = await getAuth(locale.value);
|
||||
const iniFroList = async (building_id) => {
|
||||
const res = await getAuth(building_id);
|
||||
|
||||
store.updateAuthPage(
|
||||
res.data.map((d) =>
|
||||
@ -72,23 +72,21 @@ watch(
|
||||
(newVal) => {
|
||||
if (newVal !== null) {
|
||||
getSubMonitorPage(newVal.building_guid);
|
||||
iniFroList(newVal.building_guid);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
watch(locale, () => {
|
||||
iniFroList();
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
iniFroList();
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<ul class="px-1 menu-box my-2">
|
||||
<li class="flex flex-col items-center justify-center">
|
||||
<router-link
|
||||
:to="{ name: 'dashboard' }"
|
||||
:to="{
|
||||
name:
|
||||
buildingStore.selectedBuilding?.is_headquarter === true
|
||||
? 'headquarters'
|
||||
: 'dashboard',
|
||||
}"
|
||||
class="flex lg:flex-col justify-center items-center btn-group text-white"
|
||||
>
|
||||
<font-awesome-icon
|
||||
@ -96,7 +94,7 @@ onMounted(() => {
|
||||
size="2x"
|
||||
class="w-10 m-auto"
|
||||
/>
|
||||
<span>{{ $t("home") }}</span>
|
||||
<span>{{ $t("navbar.home") }}</span>
|
||||
</router-link>
|
||||
</li>
|
||||
<li
|
||||
@ -132,11 +130,11 @@ onMounted(() => {
|
||||
size="2x"
|
||||
class="w-10 m-auto"
|
||||
/>
|
||||
<span>{{ page.subName }}</span>
|
||||
<span>{{ $t(`navbar.${page.showView}`) }}</span>
|
||||
</a>
|
||||
<router-link
|
||||
v-else
|
||||
:to="page.navigate"
|
||||
:to="`/` + page.showView"
|
||||
type="link"
|
||||
class="flex lg:flex-col justify-center items-center btn-group text-white"
|
||||
>
|
||||
@ -145,7 +143,7 @@ onMounted(() => {
|
||||
size="2x"
|
||||
class="w-10 m-auto"
|
||||
/>
|
||||
<span>{{ page.subName }}</span>
|
||||
<span>{{ $t(`navbar.${page.showView}`) }}</span>
|
||||
</router-link>
|
||||
</li>
|
||||
</ul>
|
||||
@ -169,7 +167,7 @@ onMounted(() => {
|
||||
<a-sub-menu
|
||||
v-for="main in menu_array"
|
||||
:key="main.main_system_tag"
|
||||
:title="main.full_name"
|
||||
:title="main.resource ? t(`navbar.${main.resource}`) : main.full_name"
|
||||
v-if="menu_array.length > 0 && open"
|
||||
>
|
||||
<a-menu-item
|
||||
@ -198,7 +196,7 @@ onMounted(() => {
|
||||
},
|
||||
}"
|
||||
>
|
||||
{{ sub.full_name }}
|
||||
{{ sub.resource ? $t(`navbar.${sub.resource}`): sub.full_name }}
|
||||
</router-link>
|
||||
</a-menu-item>
|
||||
</a-sub-menu>
|
||||
|
@ -13,6 +13,39 @@
|
||||
"name": "名称",
|
||||
"time": "时间"
|
||||
},
|
||||
"navbar": {
|
||||
"home": "首页",
|
||||
"sysMonBtnList": "系统监控",
|
||||
"historyData": "历史资料",
|
||||
"energyManagement": "能源管理",
|
||||
"alert": "告警",
|
||||
"operation": "运维管理",
|
||||
"graphManagement": "图资管理",
|
||||
"AssetManagement": "资产管理",
|
||||
"accountManagement": "帐号管理",
|
||||
"UserManagement": "帐号管理",
|
||||
"Setting": "系统设定",
|
||||
"energy_analysis": "能源分析",
|
||||
"consumption_report": "用电报表",
|
||||
"chart_analysis": "图表分析",
|
||||
"historical_curve": "历史曲线",
|
||||
"quick_metering": "快速抄表",
|
||||
"electricity_classification": "用电分类",
|
||||
"daily_report": "日报表",
|
||||
"weekly_report": "周报表",
|
||||
"monthly_report": "月报表",
|
||||
"annual_report": "年报表",
|
||||
"setting":"系统设置",
|
||||
"device":"设置设置",
|
||||
"department": "部门",
|
||||
"settings_2d_3d": "2D/3D 显示设置",
|
||||
"maintenance_vendor": "维修厂商",
|
||||
"building": "厂区",
|
||||
"floor": "楼层",
|
||||
"demand": "电表",
|
||||
"time_based_pricing": "时段电价",
|
||||
"mqtt_result": "MQTT 结果"
|
||||
},
|
||||
"upload": {
|
||||
"title": "选择一个文件或拖放到这里",
|
||||
"description": "档案不超过 10MB",
|
||||
@ -89,11 +122,11 @@
|
||||
"reset_value": "复归值",
|
||||
"edit_automatic_demand": "编辑自动需量",
|
||||
"elec_bills": "今年电费累计(元)",
|
||||
"interval_elec_charges": "区间电费(元)",
|
||||
"interval_elec_charges": "本月电费(元)",
|
||||
"year_carbon_emission": "今年碳排当量累计(公斤)",
|
||||
"interval_carbon_emission": "区间碳排当量",
|
||||
"interval_carbon_emission": "本月碳排当量",
|
||||
"year_elec_consumption": "今年用电度数(kWh)",
|
||||
"interval_elec_consumption": "区间用电度数(kWh)",
|
||||
"interval_elec_consumption": "本月用电度数(kWh)",
|
||||
"monthly_elec_consumption": "每月用电分析",
|
||||
"monthly_carbon_emission_and_reduction": "每月碳排当量 (kgCO2e)",
|
||||
"monthly_bill_power": "每月计费度数 (kWh)",
|
||||
@ -155,7 +188,9 @@
|
||||
"off_peak_contract": "离峰契约",
|
||||
"variable_electricity_charge": "流动电费",
|
||||
"saturday": "周六",
|
||||
"sunday_and_off_peak_days": "周日及离峰日"
|
||||
"sunday_and_off_peak_days": "周日及离峰日",
|
||||
"past_elec_data": "去年±数值(百分比)",
|
||||
"past_month_elec_data": "上月±数值(百分比)"
|
||||
},
|
||||
"alarm": {
|
||||
"title": "显示警告",
|
||||
@ -396,6 +431,7 @@
|
||||
},
|
||||
"msg": {
|
||||
"sure_to_delete": "是否确认删除该项目?",
|
||||
"is_headquarters": "该帐号为总部帐号,是否仍要删除该项目?",
|
||||
"sure_to_delete_permanent": "是否确认永久删除该项目?",
|
||||
"delete_success": "删除成功",
|
||||
"delete_failed": "删除失败",
|
||||
|
@ -10,9 +10,42 @@
|
||||
"in_otal": "筆資料",
|
||||
"skip_to": "跳至",
|
||||
"serial_number": "序號",
|
||||
"name": "姓名",
|
||||
"name": "名稱",
|
||||
"time": "時間"
|
||||
},
|
||||
"navbar": {
|
||||
"home": "首頁",
|
||||
"sysMonBtnList": "系統監控",
|
||||
"historyData": "歷史資料",
|
||||
"energyManagement": "能源管理",
|
||||
"alert": "告警",
|
||||
"operation": "運維管理",
|
||||
"graphManagement": "圖資管理",
|
||||
"AssetManagement": "資產管理",
|
||||
"accountManagement": "帳號管理",
|
||||
"UserManagement": "帳號管理",
|
||||
"Setting": "系統設定",
|
||||
"energy_analysis": "能耗分析",
|
||||
"consumption_report": "用電報表",
|
||||
"chart_analysis": "圖表分析",
|
||||
"historical_curve": "歷史曲線",
|
||||
"quick_metering": "快速抄表",
|
||||
"electricity_classification": "用電分類",
|
||||
"daily_report": "日報表",
|
||||
"weekly_report": "周報表",
|
||||
"monthly_report": "月報表",
|
||||
"annual_report": "年報表",
|
||||
"setting":"系統設定",
|
||||
"device":"設備設定",
|
||||
"department": "部門",
|
||||
"settings_2d_3d": "2D/3D 顯示設定",
|
||||
"maintenance_vendor": "維運廠商",
|
||||
"building": "廠區",
|
||||
"floor": "樓層",
|
||||
"demand": "電表",
|
||||
"time_based_pricing": "時段電價",
|
||||
"mqtt_result": "MQTT 結果"
|
||||
},
|
||||
"upload": {
|
||||
"title": "選擇一個文件或拖放到這裡",
|
||||
"description": "檔案不超過 10MB",
|
||||
@ -89,11 +122,11 @@
|
||||
"reset_value": "復歸值",
|
||||
"edit_automatic_demand": "編輯自動需量",
|
||||
"elec_bills": "今年電費累計(元)",
|
||||
"interval_elec_charges": "區間電費(元)",
|
||||
"interval_elec_charges": "本月電費(元)",
|
||||
"year_carbon_emission": "今年碳排當量累計(公斤)",
|
||||
"interval_carbon_emission": "區間碳排當量",
|
||||
"interval_carbon_emission": "本月碳排當量",
|
||||
"year_elec_consumption": "今年用電度數(kWh)",
|
||||
"interval_elec_consumption": "區間用電度數(kWh)",
|
||||
"interval_elec_consumption": "本月用電度數(kWh)",
|
||||
"monthly_elec_consumption": "每月用電分析",
|
||||
"monthly_carbon_emission_and_reduction": "每月碳排當量 (kgCO2e)",
|
||||
"monthly_bill_power": "每月計費度數 (kWh)",
|
||||
@ -155,7 +188,9 @@
|
||||
"off_peak_contract": "離峰契約",
|
||||
"variable_electricity_charge": "流動電費",
|
||||
"saturday": "週六",
|
||||
"sunday_and_off_peak_days": "週日及離峰日"
|
||||
"sunday_and_off_peak_days": "週日及離峰日",
|
||||
"past_elec_data": "去年±數值(百分比)",
|
||||
"past_month_elec_data": "上月±數值(百分比)"
|
||||
},
|
||||
"alarm": {
|
||||
"title": "顯示警告",
|
||||
@ -396,6 +431,7 @@
|
||||
},
|
||||
"msg": {
|
||||
"sure_to_delete": "是否確認刪除該項目?",
|
||||
"is_headquarters": "該帳號為總部帳號,是否仍要刪除該項目?",
|
||||
"sure_to_delete_permanent": "是否確認永久刪除該項目?",
|
||||
"delete_success": "刪除成功",
|
||||
"delete_failed": "刪除失敗",
|
||||
|
@ -13,6 +13,39 @@
|
||||
"name": "Name",
|
||||
"time": "Time"
|
||||
},
|
||||
"navbar": {
|
||||
"home": "Home",
|
||||
"sysMonBtnList": "Monitoring",
|
||||
"historyData": "History Data",
|
||||
"energyManagement": "Energy",
|
||||
"alert": "Alert",
|
||||
"operation": "Maintenance",
|
||||
"graphManagement": "Graph",
|
||||
"AssetManagement": "Devices",
|
||||
"accountManagement": "Account",
|
||||
"UserManagement": "Account",
|
||||
"Setting": "Setting",
|
||||
"energy_analysis": "Energy Analysis",
|
||||
"consumption_report": "Consumption Report",
|
||||
"chart_analysis": "Chart Analysis",
|
||||
"historical_curve": "Historical Curve",
|
||||
"quick_metering": "Quick Metering",
|
||||
"electricity_classification": "Electricity Classification",
|
||||
"daily_report": "Daily Report",
|
||||
"weekly_report": "Weekly Report",
|
||||
"monthly_report": "Monthly Report",
|
||||
"annual_report": "Annual Report",
|
||||
"setting":"Settings",
|
||||
"device":"Device",
|
||||
"department": "Department",
|
||||
"settings_2d_3d": "2D/3D Display Settings",
|
||||
"maintenance_vendor": "Vendor",
|
||||
"building": "Building",
|
||||
"floor": "Floor",
|
||||
"demand": "Demand",
|
||||
"time_based_pricing": "Time-based Pricing",
|
||||
"mqtt_result": "MQTT Result"
|
||||
},
|
||||
"upload": {
|
||||
"title": "Select a file or drag and drop here",
|
||||
"description": "File size cannot exceed 10MB",
|
||||
@ -89,11 +122,11 @@
|
||||
"reset_value": "Reset Value",
|
||||
"edit_automatic_demand": "Edit automatic demand",
|
||||
"elec_bills": "Total electricity bills this year (yuan)",
|
||||
"interval_elec_charges": "Interval electricity charges (yuan)",
|
||||
"interval_elec_charges": "Electricity charges for this month (yuan)",
|
||||
"year_carbon_emission": "Cumulative carbon emission equivalent this year (kg)",
|
||||
"interval_carbon_emission": "Interval carbon emission equivalent",
|
||||
"interval_carbon_emission": "This month's carbon emission equivalent",
|
||||
"year_elec_consumption": "This year's electricity consumption (kWh)",
|
||||
"interval_elec_consumption": "Interval electricity consumption (kWh)",
|
||||
"interval_elec_consumption": "This month's electricity consumption (kWh)",
|
||||
"monthly_elec_consumption": "Monthly electricity consumption analysis",
|
||||
"monthly_carbon_emission_and_reduction": "Monthly carbon emission equivalent (kgCO2e)",
|
||||
"monthly_bill_power": "Monthly billing power (kWh)",
|
||||
@ -155,7 +188,9 @@
|
||||
"off_peak_contract": "Off-Peak Demand Charge",
|
||||
"variable_electricity_charge": "Variable Electricity Charge",
|
||||
"saturday": "Saturday",
|
||||
"sunday_and_off_peak_days": "Sunday and Off-Peak Days"
|
||||
"sunday_and_off_peak_days": "Sunday and Off-Peak Days",
|
||||
"past_elec_data": "Last year ± value (percentage)",
|
||||
"past_month_elec_data": "Last month ± value (percentage)"
|
||||
},
|
||||
"alarm": {
|
||||
"title": "Warning",
|
||||
@ -396,6 +431,7 @@
|
||||
},
|
||||
"msg": {
|
||||
"sure_to_delete": "Are you sure to delete this item?",
|
||||
"is_headquarters": "This account is a headquarters account. Are you sure you want 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",
|
||||
|
@ -2,69 +2,49 @@ export const AUTHPAGES = [
|
||||
{
|
||||
authCode: "PF0",
|
||||
icon: "home",
|
||||
navigate: "/dashboard",
|
||||
},
|
||||
{
|
||||
authCode: "PF1",
|
||||
icon: "tv",
|
||||
navigate: "/system",
|
||||
},
|
||||
{
|
||||
authCode: "PF2",
|
||||
icon: "chart-pie",
|
||||
pageName: "energyManagement",
|
||||
navigate: "/energyManagement",
|
||||
},
|
||||
{
|
||||
authCode: "PF3",
|
||||
icon: "chart-area",
|
||||
navigate: "/historyData",
|
||||
},
|
||||
{
|
||||
authCode: "PF4",
|
||||
icon: "chart-line",
|
||||
navigate: "/historyData",
|
||||
},
|
||||
{
|
||||
authCode: "PF5",
|
||||
icon: "bell",
|
||||
pageName: "alert",
|
||||
navigate: "/alert",
|
||||
},
|
||||
{
|
||||
authCode: "PF6",
|
||||
icon: "server",
|
||||
pageName: "operation",
|
||||
navigate: "/operation",
|
||||
},
|
||||
{
|
||||
authCode: "PF7",
|
||||
icon: "image",
|
||||
pageName: "graphManagement",
|
||||
navigate: "/graphManagement",
|
||||
},
|
||||
{
|
||||
authCode: "PF8",
|
||||
icon: "user",
|
||||
pageName: "accountManagement",
|
||||
navigate: "/accountManagement",
|
||||
},
|
||||
{
|
||||
authCode: "PF9",
|
||||
icon: "database",
|
||||
pageName: "AssetManagement",
|
||||
navigate: "/assetManagement",
|
||||
},
|
||||
{
|
||||
authCode: "PF10",
|
||||
icon: "leaf",
|
||||
pageName: "ProductSetting",
|
||||
navigate: "/productSetting",
|
||||
},
|
||||
{
|
||||
authCode: "PF11",
|
||||
icon: "cog",
|
||||
pageName: "Setting",
|
||||
navigate: "/Setting",
|
||||
},
|
||||
];
|
||||
|
@ -18,6 +18,7 @@ export default function useForgeHeatmap() {
|
||||
|
||||
//create the heatmap
|
||||
function getSensorValue(device, sensorType, pointData) {
|
||||
console.log("heatmap", device, realtimeData.value);
|
||||
const dev = realtimeData.value.find(
|
||||
({ device_number }) => device_number === device.id
|
||||
);
|
||||
|
@ -9,8 +9,11 @@ import AlertManagement from "@/views/alert/AlertManagement.vue";
|
||||
import ProductSetting from "@/views/productSetting/ProductSetting.vue";
|
||||
import EnergyManagement from "@/views/energyManagement/EnergyManagement.vue";
|
||||
import SettingManagement from "@/views/setting/SettingManagement.vue";
|
||||
import HeadquartersManagement from "@/views/headquarters/HeadquartersManagement.vue";
|
||||
import UserManagement from "@/views/headquarters/HeadquartersAccountManagement.vue";
|
||||
import Login from "@/views/login/Login.vue";
|
||||
import useUserInfoStore from "@/stores/useUserInfoStore";
|
||||
import useBuildingStore from "@/stores/useBuildingStore";
|
||||
import useGetCookie from "@/hooks/useGetCookie";
|
||||
import System from "@/views/system/System.vue";
|
||||
import SystemFloor from "@/views/system/SystemFloor.vue";
|
||||
@ -90,6 +93,16 @@ const router = createRouter({
|
||||
name: "setting",
|
||||
component: SettingManagement,
|
||||
},
|
||||
{
|
||||
path: "/headquarters",
|
||||
name: "headquarters",
|
||||
component: HeadquartersManagement,
|
||||
},
|
||||
{
|
||||
path: "/UserManagement",
|
||||
name: "UserManagement",
|
||||
component: UserManagement,
|
||||
},
|
||||
{
|
||||
path: "/mytestfile/mjm",
|
||||
name: "mytestfile",
|
||||
@ -106,9 +119,11 @@ router.beforeEach(async (to, from, next) => {
|
||||
const auth = useUserInfoStore();
|
||||
const token = useGetCookie("JWT-Authorization");
|
||||
const user_name = useGetCookie("user_name");
|
||||
const buildingStore = useBuildingStore();
|
||||
|
||||
if ((authRequired && !token) || to.path === "/") {
|
||||
auth.user.token = "";
|
||||
buildingStore.deleteBuilding();
|
||||
next({ path: "/login" });
|
||||
} else if (!authRequired) {
|
||||
document.cookie = "JWT-Authorization=; Max-Age=0";
|
||||
|
@ -84,8 +84,8 @@ const useBuildingStore = defineStore("buildingInfo", () => {
|
||||
// 獲取2D、3D顯示與否
|
||||
const fetchDashboard2D3D = async (BuildingId) => {
|
||||
const res = await getDashboard2D3D(BuildingId);
|
||||
showForgeArea.value = res.data.is3DEnabled;
|
||||
previewImageExt.value = res.data.previewImageExt || "";
|
||||
showForgeArea.value = res.data?.is3DEnabled || false;
|
||||
previewImageExt.value = res.data?.previewImageExt || "";
|
||||
};
|
||||
|
||||
// 清除localStorage建築物
|
||||
|
@ -4,7 +4,6 @@ import { ref } from "vue";
|
||||
const useUserInfoStore = defineStore("userInfo", () => {
|
||||
const user = ref({
|
||||
token: "",
|
||||
expires: 0,
|
||||
user_name:"",
|
||||
});
|
||||
|
||||
|
@ -12,13 +12,23 @@ const { searchParams, changeParams } = useSearchParam();
|
||||
const companyOptions = ref([]);
|
||||
const iotSchemaOptions = ref([]);
|
||||
const elecTypeOptions = ref([]);
|
||||
const iotSchemaTag = ref(""); // 儲存 IOT Schema 的 tagIoT
|
||||
const getCompany = async () => {
|
||||
const res = await getOperationCompanyList();
|
||||
companyOptions.value = res.data.map((d) => ({ ...d, key: d.id }));
|
||||
};
|
||||
const getIOTSchemaOptions = async (id) => {
|
||||
const res = await getIOTSchema(Number(id));
|
||||
iotSchemaOptions.value = res.data.map((d) => ({ ...d, key: d.id }));
|
||||
const data = res.data || [];
|
||||
|
||||
iotSchemaOptions.value = data.map((d) => ({ ...d, key: d.id }));
|
||||
|
||||
// 取出第一筆的 tagIoT,提供給最深層元件使用
|
||||
if (data.length > 0 && data[0].tagIoT) {
|
||||
iotSchemaTag.value = data[0].tagIoT;
|
||||
} else {
|
||||
iotSchemaTag.value = "";
|
||||
}
|
||||
};
|
||||
const getElecType = async () => {
|
||||
const res = await getElecTypeList();
|
||||
@ -51,6 +61,7 @@ provide("asset_modal_options", {
|
||||
elecTypeOptions,
|
||||
departmentList,
|
||||
floors,
|
||||
iotSchemaTag
|
||||
});
|
||||
</script>
|
||||
|
||||
|
@ -148,7 +148,9 @@ const edit = async (id) => {
|
||||
// changeParams({ ...searchParams.value, main_id: record.id });
|
||||
const res = await getAssetSingle(id);
|
||||
if (res.isSuccess) {
|
||||
res.data.oriFile = res.data.oriFile.map((file, index) => ({
|
||||
res.data.oriFile = res.data.oriFile
|
||||
.filter((file) => file.saveName)
|
||||
.map((file, index) => ({
|
||||
...file,
|
||||
key: index,
|
||||
src: file.file_url,
|
||||
|
@ -6,7 +6,9 @@ import AssetTableModalLeft from "./AssetTableModalLeft.vue";
|
||||
import AssetTableModalRight from "./AssetTableModalRight.vue";
|
||||
import useFormErrorMessage from "@/hooks/useFormErrorMessage";
|
||||
import * as yup from "yup";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
const { t } = useI18n();
|
||||
const { openToast } = inject("app_toast");
|
||||
const { searchParams, changeParams } = useSearchParam();
|
||||
|
||||
@ -75,8 +77,11 @@ const onOk = async () => {
|
||||
main_id: props.editRecord ? props.editRecord.main_id : 0,
|
||||
});
|
||||
if (res.isSuccess) {
|
||||
openToast("success", t("msg.send_successfully"), "#asset_add_table_item");
|
||||
props.getData();
|
||||
setTimeout(() => {
|
||||
closeModal();
|
||||
}, 1000);
|
||||
} else {
|
||||
openToast("error", res.msg, "#asset_add_table_item");
|
||||
}
|
||||
@ -102,12 +107,20 @@ const closeModal = () => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<button class="btn btn-sm btn-add mr-3" @click.stop.prevent="openModal" :disabled="!searchParams.subSys_id">
|
||||
<button
|
||||
class="btn btn-sm btn-add mr-3"
|
||||
@click.stop.prevent="openModal"
|
||||
:disabled="!searchParams.subSys_id"
|
||||
>
|
||||
<font-awesome-icon :icon="['fas', 'plus']" />{{ $t("button.add") }}
|
||||
</button>
|
||||
<Modal
|
||||
id="asset_add_table_item"
|
||||
:title="editRecord?.main_id ? $t('assetManagement.edit_device') : $t('assetManagement.add_device')"
|
||||
:title="
|
||||
editRecord?.main_id
|
||||
? $t('assetManagement.edit_device')
|
||||
: $t('assetManagement.add_device')
|
||||
"
|
||||
:onCancel="closeModal"
|
||||
:width="1600"
|
||||
>
|
||||
|
@ -32,7 +32,9 @@ const getMenuData = async () => {
|
||||
const getData = async (id) => {
|
||||
const res = await getGraphData(id);
|
||||
if (res.isSuccess) {
|
||||
dataSource.value = res.data.map((d) => ({ ...d, key: d.id }));
|
||||
dataSource.value = res.data
|
||||
.filter((d) => d.oriSavName)
|
||||
.map((d) => ({ ...d, key: d.id }));
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -1,102 +1,126 @@
|
||||
<script setup>
|
||||
import { onMounted, ref, inject, watch, computed } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { postMQTTpublish } from "@/apis/asset";
|
||||
import { postMQTTpublish, postMqttTopic, postMqttTopicStop } from "@/apis/asset";
|
||||
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 BASEURL = import.meta.env.VITE_MQTT_BASEURL;
|
||||
const { iotSchemaTag } = inject("asset_modal_options");
|
||||
|
||||
// MQTT相關
|
||||
const mqttClient = ref(null); // MQTT客戶端
|
||||
const receivedMessages = ref([]); // 儲存接收到的訊息
|
||||
const countdown = ref(60); // 倒計時初始為 60 秒
|
||||
const hasStartedCountdown = ref(false); // 是否已開始倒數
|
||||
let timer = null; // 記錄計時器
|
||||
let mqttInterval = null;
|
||||
const mqttCardDataList = ref([]); // 顯示在畫面上的卡片資料
|
||||
|
||||
const openModal = () => {
|
||||
if (!mqttClient.value) {
|
||||
connectMqtt();
|
||||
}
|
||||
const openModal = async () => {
|
||||
mqtt_test.showModal();
|
||||
startCountdown(); // 開始倒計時
|
||||
};
|
||||
|
||||
const connectMqtt = () => {
|
||||
const topic = formState.value.topic || ""; // 取得主題
|
||||
const mqttHost = `${BASEURL}`;
|
||||
const protocol = "wss"; // 根據伺服器配置,需要設置為 "ws" 或 "wss"
|
||||
mqttClient.value = mqtt.connect(mqttHost, {
|
||||
protocol,
|
||||
reconnectPeriod: 1000, // 每秒嘗試重新連線
|
||||
username: "admin", // MQTT 帳號
|
||||
password: "mjmadmin@99", // MQTT 密碼
|
||||
port: 443,
|
||||
// 先立即呼叫一次
|
||||
try {
|
||||
await postMqttTopic({
|
||||
iotTag: iotSchemaTag?.value,
|
||||
Topic: formState.value.topic,
|
||||
});
|
||||
// console.log("首次 postMqttTopic API 已呼叫");
|
||||
} catch (error) {
|
||||
console.error("首次 postMqttTopic 發送失敗", error);
|
||||
}
|
||||
|
||||
// 啟動每 5 秒重複呼叫
|
||||
mqttInterval = setInterval(async () => {
|
||||
try {
|
||||
const res = await postMqttTopic({
|
||||
iotTag: iotSchemaTag?.value,
|
||||
Topic: formState.value.topic,
|
||||
});
|
||||
|
||||
mqttClient.value.on("connect", () => {
|
||||
console.log("MQTT 已連接");
|
||||
if (topic) {
|
||||
mqttClient.value.subscribe(topic, (err) => {
|
||||
if (!err) {
|
||||
console.log(`已訂閱主題: ${topic}`);
|
||||
// console.log("postMqttTopic 回傳:", res?.data);
|
||||
|
||||
const payload = res?.data;
|
||||
|
||||
// 確保 payload 結構正確
|
||||
if (payload && payload.data && payload.time) {
|
||||
const timeAlreadyExists = mqttCardDataList.value.some(
|
||||
(item) => item.time === payload.time
|
||||
);
|
||||
|
||||
if (!timeAlreadyExists) {
|
||||
mqttCardDataList.value.unshift({
|
||||
...payload.data,
|
||||
time: payload.time,
|
||||
});
|
||||
|
||||
// ⬇第一次收到資料才開始倒數
|
||||
if (!hasStartedCountdown.value) {
|
||||
hasStartedCountdown.value = true;
|
||||
startCountdown();
|
||||
}
|
||||
} else {
|
||||
console.error("訂閱失敗: ", err);
|
||||
// console.log("已存在相同時間略過:", payload.time);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// console.warn("回傳資料結構錯誤或缺少 time");
|
||||
}
|
||||
});
|
||||
|
||||
mqttClient.value.on("message", (topic, message) => {
|
||||
// 儲存接收到的訊息
|
||||
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); // 收到訊息後清除倒計時
|
||||
});
|
||||
|
||||
mqttClient.value.on("error", (err) => {
|
||||
console.error("MQTT 連線錯誤: ", err);
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("postMqttTopic 呼叫失敗:", error);
|
||||
}
|
||||
}, 5000);
|
||||
};
|
||||
|
||||
const startCountdown = () => {
|
||||
if (timer) return; // 防止重複啟動計時器
|
||||
countdown.value = 60;
|
||||
|
||||
timer = setInterval(() => {
|
||||
if (countdown.value > 0) {
|
||||
if (countdown.value > 1) {
|
||||
countdown.value--;
|
||||
} else {
|
||||
onCancel(); // 1分鐘後如果沒有收到訊息則觸發 onCancel
|
||||
clearInterval(timer);
|
||||
timer = null;
|
||||
onCancel(); // 60秒結束自動關閉
|
||||
}
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
const onCancel = () => {
|
||||
const onCancel = async () => {
|
||||
// 清空資料與狀態
|
||||
receivedMessages.value = [];
|
||||
mqttCardDataList.value = [];
|
||||
countdown.value = 60;
|
||||
hasStartedCountdown.value = false;
|
||||
|
||||
mqtt_test.close();
|
||||
|
||||
// 斷開 MQTT 連線
|
||||
if (mqttClient.value) {
|
||||
mqttClient.value.end();
|
||||
mqttClient.value = null;
|
||||
// 停止 API 呼叫
|
||||
try {
|
||||
await postMqttTopicStop({
|
||||
iotTag: iotSchemaTag?.value,
|
||||
Topic: formState.value.topic,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("postMqttTopicStop 發送失敗", error);
|
||||
}
|
||||
|
||||
// 重置倒計時
|
||||
// 清除 API Interval
|
||||
if (mqttInterval) {
|
||||
clearInterval(mqttInterval);
|
||||
mqttInterval = null;
|
||||
}
|
||||
|
||||
// 清除倒數 Timer
|
||||
if (timer) {
|
||||
clearInterval(timer);
|
||||
timer = null; // 清除計時器引用
|
||||
timer = null;
|
||||
}
|
||||
countdown.value = 60;
|
||||
};
|
||||
|
||||
const onSubmit = async () => {
|
||||
const Topic = formState.value.topic;
|
||||
const Topic = formState.value.topic_publish;
|
||||
let Payload = "";
|
||||
try {
|
||||
Payload = JSON.stringify(JSON.parse(formState.value.publish_message));
|
||||
@ -130,7 +154,7 @@ const onSubmit = async () => {
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col col-span-2 border-t-gray-400 border-t py-5">
|
||||
<Input :value="formState" name="topic">
|
||||
<Input :value="formState" name="topic_publish">
|
||||
<template #topLeft>MQTT publish topic</template>
|
||||
</Input>
|
||||
<Textarea :value="formState" name="publish_message">
|
||||
@ -142,42 +166,69 @@ const onSubmit = async () => {
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<Modal id="mqtt_test" title="MQTT Topic" :onCancel="onCancel" :width="400">
|
||||
<Modal id="mqtt_test" title="MQTT Topic" :onCancel="onCancel" width="400">
|
||||
<template #modalContent>
|
||||
<!-- 顯示接收到的訊息 -->
|
||||
<div v-if="receivedMessages.length > 0" class="overflow-y-auto h-96">
|
||||
<div v-if="mqttCardDataList.length > 0" class="overflow-y-auto h-96 mt-4">
|
||||
<ul>
|
||||
<li
|
||||
v-for="(message, index) in receivedMessages"
|
||||
v-for="(item, index) in mqttCardDataList"
|
||||
: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
|
||||
<strong
|
||||
class="text-base block text-info mb-2 flex justify-between items-center"
|
||||
>
|
||||
<p class="text-sm break-words">{{ message.message }}</p>
|
||||
<p class="text-xs text-slate-200 pt-2">
|
||||
<span>
|
||||
<FontAwesomeIcon :icon="['fas', 'clock']" class="me-1" />
|
||||
{{ message.timestamp }}
|
||||
{{ dayjs(item.time).format("YYYY-MM-DD HH:mm:ss") }}
|
||||
</span>
|
||||
|
||||
<!-- 只在第一筆資料加上「最新」標籤 -->
|
||||
<span
|
||||
v-if="index === 0"
|
||||
class="text-xs text-white bg-green-600 px-2 py-1 rounded"
|
||||
>
|
||||
New
|
||||
</span>
|
||||
</strong>
|
||||
|
||||
<!-- 動態顯示除了 time 以外的所有欄位 -->
|
||||
<template v-for="[key, value] in Object.entries(item)" :key="key">
|
||||
<p v-if="key !== 'time'" class="text-sm break-words">
|
||||
<strong>{{ key }}:</strong>{{ value }}
|
||||
</p>
|
||||
</template>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<!-- 顯示 loading 和倒計時 -->
|
||||
<p v-else class="text-center mt-20">
|
||||
|
||||
<!-- 顯示 loading 和倒計時(只有沒資料才顯示) -->
|
||||
<p v-if="mqttCardDataList.length === 0" class="text-center mt-20">
|
||||
<Loading />
|
||||
<br />
|
||||
<span class="text-base">{{ countdown }} seconds</span>
|
||||
<span class="text-base">Loading...</span>
|
||||
</p>
|
||||
</template>
|
||||
<template #modalAction>
|
||||
<div class="relative w-full flex justify-end items-center gap-12">
|
||||
<!-- 資料出現後才顯示倒數計時,置中顯示 -->
|
||||
<div
|
||||
v-if="mqttCardDataList.length > 0"
|
||||
class="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 flex items-center gap-2 text-sm"
|
||||
>
|
||||
<span>Auto close in</span>
|
||||
<span>{{ countdown }}s</span>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="reset"
|
||||
class="btn btn-outline-success mr-2"
|
||||
class="btn btn-outline-success"
|
||||
@click.prevent="onCancel"
|
||||
>
|
||||
{{ t("button.cancel") }}
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
</Modal>
|
||||
</template>
|
||||
|
@ -127,8 +127,9 @@ const resetModalForm = () => {
|
||||
};
|
||||
};
|
||||
|
||||
const removeAccount = async (id) => {
|
||||
openToast("warning", t("msg.sure_to_delete"), "body", async () => {
|
||||
const removeAccount = async (id, notHeadquarters) => {
|
||||
const message = notHeadquarters ? t("msg.sure_to_delete") : t("msg.is_headquarters");
|
||||
openToast("warning", message, "body", async () => {
|
||||
await cancelToastOpen();
|
||||
const res = await delAccount(id);
|
||||
if (res.isSuccess) {
|
||||
@ -202,7 +203,7 @@ const removeAccount = async (id) => {
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-sm btn-error text-white"
|
||||
@click.stop.prevent="() => removeAccount(record.userinfo_guid)"
|
||||
@click.stop.prevent="() => removeAccount(record.userinfo_guid,record.canDeleted)"
|
||||
>
|
||||
{{ $t("button.delete") }}
|
||||
</button>
|
||||
|
@ -35,16 +35,23 @@ watch(
|
||||
(newBuilding) => {
|
||||
if (newBuilding) {
|
||||
formState.value.building_guid = newBuilding.building_guid;
|
||||
imgBaseUrl.value = store.previewImageExt
|
||||
? `${FILE_BASEURL}/upload/setting/previewImage/${newBuilding.building_guid}${store.previewImageExt}`
|
||||
: import.meta.env.MODE === "production"
|
||||
? "dist/build_img.jpg"
|
||||
: "/build_img.jpg";
|
||||
}
|
||||
},
|
||||
{ immediate: true, deep: true }
|
||||
);
|
||||
|
||||
watch(
|
||||
[() => store.previewImageExt, () => formState.value.building_guid],
|
||||
([newExt, buildingGuid]) => {
|
||||
if (newExt && buildingGuid) {
|
||||
imgBaseUrl.value = `${FILE_BASEURL}/upload/setting/previewImage/${buildingGuid}${newExt}`;
|
||||
} else {
|
||||
imgBaseUrl.value = `${FILE_BASEURL}/upload/build_img.jpg`;
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
watch(
|
||||
() => formState.value,
|
||||
(newVal) => {
|
||||
@ -139,7 +146,7 @@ onUnmounted(() => {
|
||||
<template v-else>
|
||||
<img
|
||||
alt="build"
|
||||
:src="imgBaseUrl || '/build_img.jpg'"
|
||||
:src="imgBaseUrl"
|
||||
class="area-img-box w-full h-[460px] block relative rounded-sm mt-3"
|
||||
/>
|
||||
</template>
|
||||
|
@ -88,7 +88,7 @@ watch(
|
||||
<th
|
||||
class="text-base border text-white text-center bg-cyan-600 bg-opacity-30"
|
||||
>
|
||||
{{ $t("table.time") }}
|
||||
{{ $t("operation.updated_time") }}
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
@ -28,6 +28,7 @@ const {
|
||||
} = useActiveBtn("multiple");
|
||||
|
||||
const taipower_data = ref([]);
|
||||
const yearly_taipower_data = ref([]);
|
||||
const carbonValue = ref(null);
|
||||
const search_data = computed(() => {
|
||||
return {
|
||||
@ -41,9 +42,10 @@ const search_data = computed(() => {
|
||||
const getData = async (value) => {
|
||||
const res = await getTaipower(value);
|
||||
if (res.isSuccess) {
|
||||
taipower_data.value = res.data
|
||||
? res.data.sort((a, b) => a.month.localeCompare(b.month))
|
||||
taipower_data.value = res.data?.monthlyUsage
|
||||
? res.data?.monthlyUsage.sort((a, b) => a.month.localeCompare(b.month))
|
||||
: [];
|
||||
yearly_taipower_data.value = res.data?.yearlyUsage;
|
||||
}
|
||||
};
|
||||
|
||||
@ -96,7 +98,12 @@ watch(
|
||||
}
|
||||
);
|
||||
|
||||
provide("energy_data", { taipower_data, search_data, carbonValue });
|
||||
provide("energy_data", {
|
||||
taipower_data,
|
||||
yearly_taipower_data,
|
||||
search_data,
|
||||
carbonValue,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
@ -4,7 +4,7 @@ import { twMerge } from "tailwind-merge";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
const { t, locale } = useI18n();
|
||||
const { taipower_data } = inject("energy_data");
|
||||
const { taipower_data, yearly_taipower_data } = inject("energy_data");
|
||||
|
||||
const daysInMonth = (month) => {
|
||||
const [year, monthNumber] = month.split("-");
|
||||
@ -19,7 +19,10 @@ const calculateData = () => {
|
||||
item.month.startsWith(currentYear)
|
||||
);
|
||||
|
||||
const totalElecBills = filteredData.reduce((sum, item) => sum + item.costTotal, 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;
|
||||
@ -37,29 +40,53 @@ const calculateData = () => {
|
||||
);
|
||||
|
||||
return [
|
||||
{ title: t("energy.elec_bills"), data: totalElecBills.toLocaleString() },
|
||||
{
|
||||
title: t("energy.elec_bills"),
|
||||
data: totalElecBills.toLocaleString(),
|
||||
past: yearly_taipower_data.value.costTotalGrowthPercent
|
||||
? yearly_taipower_data.value.costTotalGrowthPercent.toLocaleString()
|
||||
: 0,
|
||||
},
|
||||
{
|
||||
title: t("energy.interval_elec_charges"),
|
||||
time: monthTxt,
|
||||
data: latestMonthData ? latestMonthData.costTotal.toLocaleString() : 0,
|
||||
past:
|
||||
latestMonthData && latestMonthData.costTotalGrowthPercent
|
||||
? latestMonthData.costTotalGrowthPercent.toLocaleString()
|
||||
: 0,
|
||||
},
|
||||
{
|
||||
title: t("energy.year_carbon_emission"),
|
||||
data: totalCarbonEmission.toLocaleString(),
|
||||
past: yearly_taipower_data.value.carbonGrowthPercent
|
||||
? yearly_taipower_data.value.carbonGrowthPercent.toLocaleString()
|
||||
: 0,
|
||||
},
|
||||
{
|
||||
title: t("energy.interval_carbon_emission"),
|
||||
time: monthTxt,
|
||||
data: latestMonthData ? latestMonthData.carbon.toLocaleString() : 0,
|
||||
past:
|
||||
latestMonthData && latestMonthData.carbonGrowthPercent
|
||||
? latestMonthData.carbonGrowthPercent.toLocaleString()
|
||||
: 0,
|
||||
},
|
||||
{
|
||||
title: t("energy.year_elec_consumption"),
|
||||
data: totalElecConsumption.toLocaleString(),
|
||||
past: yearly_taipower_data.value.kWhGrowthPercent
|
||||
? yearly_taipower_data.value.kWhGrowthPercent.toLocaleString()
|
||||
: 0,
|
||||
},
|
||||
{
|
||||
title: t("energy.interval_elec_consumption"),
|
||||
time: monthTxt,
|
||||
data: latestMonthData ? latestMonthData.kWh.toLocaleString() : 0,
|
||||
past:
|
||||
latestMonthData && latestMonthData.kWhGrowthPercent
|
||||
? latestMonthData.kWhGrowthPercent.toLocaleString()
|
||||
: 0,
|
||||
},
|
||||
];
|
||||
};
|
||||
@ -95,7 +122,9 @@ watch(
|
||||
>
|
||||
<div class="title">{{ item.title }}</div>
|
||||
<div v-if="item.time" class="time">{{ item.time }}</div>
|
||||
<div class="data">{{ item.data }}</div>
|
||||
<div class="data">
|
||||
<span>{{ item.data }}</span> / <span>{{ item.past }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
123
src/views/headquarters/HeadquartersAccountManagement.vue
Normal file
123
src/views/headquarters/HeadquartersAccountManagement.vue
Normal file
@ -0,0 +1,123 @@
|
||||
<script setup>
|
||||
import Table from "@/components/customUI/Table.vue";
|
||||
// import AccountModal from "./AccountModal.vue";
|
||||
import {
|
||||
getUserList
|
||||
} from "@/apis/headquarters";
|
||||
import { onMounted, ref, inject, computed } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
const { t } = useI18n();
|
||||
const { openToast, cancelToastOpen } = inject("app_toast");
|
||||
|
||||
const dataSource = ref([]);
|
||||
const loading = ref(false);
|
||||
const searchData = ref({
|
||||
Full_name: "",
|
||||
Role_full_name: "",
|
||||
});
|
||||
const columns = computed(() => [
|
||||
{
|
||||
title: t("accountManagement.index"),
|
||||
key: "index",
|
||||
},
|
||||
{
|
||||
title: t("accountManagement.name"),
|
||||
key: "full_name",
|
||||
filter: true,
|
||||
},
|
||||
{
|
||||
title: t("accountManagement.account"),
|
||||
key: "account",
|
||||
},
|
||||
{
|
||||
title: t("accountManagement.role"),
|
||||
key: "role_full_name",
|
||||
},
|
||||
{
|
||||
title: t("accountManagement.email"),
|
||||
key: "email",
|
||||
},
|
||||
{
|
||||
title: t("accountManagement.phone"),
|
||||
key: "phone",
|
||||
},
|
||||
{
|
||||
title: t("accountManagement.created_at"),
|
||||
key: "created_at",
|
||||
},
|
||||
{
|
||||
title: t("accountManagement.operation"),
|
||||
key: "operation",
|
||||
},
|
||||
]);
|
||||
|
||||
const getDataSource = async () => {
|
||||
loading.value = true;
|
||||
const res = await getUserList();
|
||||
dataSource.value = res.data?.users || [];
|
||||
loading.value = false;
|
||||
};
|
||||
|
||||
const onSearch = () => {
|
||||
getDataSource();
|
||||
};
|
||||
|
||||
const onReset = () => {
|
||||
getDataSource();
|
||||
};
|
||||
|
||||
const getUser = async (id) => {
|
||||
// const res = await getAccountOneUser(id);
|
||||
openModal(res.data);
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
getDataSource();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<h1 class="text-2xl font-extrabold mb-2">
|
||||
{{ $t("accountManagement.account_title") }}
|
||||
</h1>
|
||||
<Table :columns="columns" :dataSource="dataSource" :loading="loading">
|
||||
<template #beforeTable>
|
||||
<div class="flex items-center mb-8">
|
||||
<Input
|
||||
:placeholder="t('accountManagement.name_placeholder')"
|
||||
name="Full_name"
|
||||
:value="searchData"
|
||||
class="mr-3 w-96"
|
||||
/>
|
||||
<Input
|
||||
:placeholder="t('accountManagement.role_placeholder')"
|
||||
name="Role_full_name"
|
||||
:value="searchData"
|
||||
/>
|
||||
<button class="btn btn-search ml-5" @click.stop.prevent="onSearch">
|
||||
<font-awesome-icon :icon="['fas', 'search']" />
|
||||
{{ $t("button.search") }}
|
||||
</button>
|
||||
<button class="btn btn-neutral mx-4" @click.stop.prevent="onReset">
|
||||
{{ $t("button.reset") }}
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
<template #bodyCell="{ record, column, index }">
|
||||
<template v-if="column.key === 'index'">{{ index + 1 }}</template>
|
||||
<template v-else-if="column.key === 'operation'">
|
||||
<button
|
||||
class="btn btn-sm btn-success text-white mr-2"
|
||||
@click.stop.prevent="() => getUser(record.userinfo_guid)"
|
||||
>
|
||||
{{ $t("button.edit") }}
|
||||
</button>
|
||||
</template>
|
||||
<template v-else>
|
||||
{{ record[column.key] }}
|
||||
</template>
|
||||
</template>
|
||||
</Table>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped></style>
|
50
src/views/headquarters/HeadquartersManagement.vue
Normal file
50
src/views/headquarters/HeadquartersManagement.vue
Normal file
@ -0,0 +1,50 @@
|
||||
<script setup>
|
||||
import { ref, computed, watch, onUnmounted } from "vue";
|
||||
import SysMap from "./components/SysMap.vue";
|
||||
import SysProgress from "./components/SysProgress.vue";
|
||||
import ElecRank from "./components/ElecRank.vue";
|
||||
import ElecTrends from "./components/ElecTrends.vue";
|
||||
import ElecCompare from "./components/ElecCompare.vue";
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="grid grid-cols-1 xl:grid-cols-4 gap-4 my-2">
|
||||
<div class="col-span-1 grid grid-cols-1 md:grid-cols-2 md:grid-rows-1 xl:grid-cols-1 xl:grid-rows-2 gap-4">
|
||||
<div class="area-img-box">
|
||||
<img
|
||||
alt="build"
|
||||
src="/build_img.jpg"
|
||||
class="w-full object-cover border-cyan-400 shadow-cyan-500/40"
|
||||
/>
|
||||
<p
|
||||
class="p-4 h-full text-gray-100 text-base font-light bg-gray-800/60 backdrop-blur-md border-t border-cyan-400/30 shadow-inner"
|
||||
>
|
||||
深耕電子精密連接器、光通信元件、軟性排線、線纜組件、PCBA電子機板、電子成品專業製造廠並代理電子零組件做為整合行銷。公司創立於1990年,產品行銷全球以穩定,快速以及高品質知名;
|
||||
未來,瀚荃會持續精進提供更快、更好以及高附加價值的產品與服務來滿足您的需求。
|
||||
</p>
|
||||
</div>
|
||||
<!--在線狀態-->
|
||||
<SysProgress />
|
||||
</div>
|
||||
<div class="col-span-2 h-full border border-cyan-400 shadow-md shadow-cyan-500/40">
|
||||
<img
|
||||
src="/CviLux_globalmap.png"
|
||||
alt=""
|
||||
class="w-full h-full"
|
||||
/>
|
||||
</div>
|
||||
<div class="col-span-1 grid grid-cols-1 xl:grid-rows-3 gap-4">
|
||||
<ElecRank />
|
||||
<ElecTrends
|
||||
/>
|
||||
<ElecCompare />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.area-img-box {
|
||||
@apply border border-light-info bg-gray-900/80 backdrop-blur-lg relative overflow-hidden shadow-md shadow-blue-300;
|
||||
}
|
||||
</style>
|
316
src/views/headquarters/components/ElecCompare.vue
Normal file
316
src/views/headquarters/components/ElecCompare.vue
Normal file
@ -0,0 +1,316 @@
|
||||
<script setup>
|
||||
import { ref, onMounted, watch, computed, onUnmounted } from "vue";
|
||||
import * as echarts from "echarts";
|
||||
import { getSystemEnergyCostGrowth } from "@/apis/headquarters";
|
||||
import BarChart from "@/components/chart/BarChart.vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
const { locale, t } = useI18n();
|
||||
|
||||
const energyCostGrowthData = ref({ day: [], week: [], month: [], year: [] });
|
||||
|
||||
const chartData = ref([]);
|
||||
const currentType = ref({
|
||||
name: "day",
|
||||
});
|
||||
const energyTypeList = ref([
|
||||
{
|
||||
title: t("dashboard.daily_relative_change"),
|
||||
key: "day",
|
||||
},
|
||||
{
|
||||
title: t("dashboard.weekly_relative_change"),
|
||||
key: "week",
|
||||
},
|
||||
{
|
||||
title: t("dashboard.monthly_relative_change"),
|
||||
key: "month",
|
||||
},
|
||||
{
|
||||
title: t("dashboard.yearly_relative_change"),
|
||||
key: "year",
|
||||
},
|
||||
]);
|
||||
let intervalId = null;
|
||||
const labels = computed(() => {
|
||||
switch (currentType.value.name) {
|
||||
case "day":
|
||||
return [t("dashboard.today"), t("dashboard.yesterday")];
|
||||
case "week":
|
||||
return [t("dashboard.this_week"), t("dashboard.last_week")];
|
||||
case "month":
|
||||
return [t("dashboard.this_month"), t("dashboard.last_month")];
|
||||
case "year":
|
||||
return [t("dashboard.this_year"), t("dashboard.last_year")];
|
||||
default:
|
||||
return [t("dashboard.today"), t("dashboard.yesterday")];
|
||||
}
|
||||
});
|
||||
const barWidth = 30; // Set barWidth
|
||||
|
||||
const barChartOptions = computed(() => ({
|
||||
xAxis: {
|
||||
type: "category",
|
||||
data: chartData.value.map((item) => item.name),
|
||||
axisLine: { lineStyle: { color: "#fff" } },
|
||||
},
|
||||
yAxis: { type: "value", show: false },
|
||||
grid: {
|
||||
left: "-10%",
|
||||
right: "1%",
|
||||
bottom: "3%",
|
||||
top: "10%",
|
||||
containLabel: true,
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: "當前",
|
||||
data: chartData.value.map((item) => item.current),
|
||||
type: "bar",
|
||||
barWidth: barWidth,
|
||||
barGap: "-10%",
|
||||
itemStyle: {
|
||||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{ offset: 0, color: "#186B80" },
|
||||
{ offset: 1, color: "#50C3E3" },
|
||||
]),
|
||||
shadowBlur: 5,
|
||||
shadowColor: "rgba(0, 0, 0, 0.3)",
|
||||
shadowOffsetY: 2,
|
||||
shadowOffsetX: 5,
|
||||
},
|
||||
z: 3,
|
||||
},
|
||||
{
|
||||
name: "對比",
|
||||
data: chartData.value.map((item) => item.last),
|
||||
type: "bar",
|
||||
barWidth: barWidth,
|
||||
itemStyle: {
|
||||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{ offset: 0, color: "#988F2C" },
|
||||
{ offset: 1, color: "#FFF26D" },
|
||||
]),
|
||||
shadowBlur: 5,
|
||||
shadowColor: "rgba(0, 0, 0, 0.3)",
|
||||
shadowOffsetY: 2,
|
||||
shadowOffsetX: 5,
|
||||
},
|
||||
},
|
||||
{
|
||||
// this top
|
||||
z: 6,
|
||||
type: "pictorialBar",
|
||||
symbolPosition: "end",
|
||||
data: chartData.value.map((item) => item.current),
|
||||
symbol: "diamond",
|
||||
symbolOffset: ["-45%", "-50%"],
|
||||
symbolSize: [barWidth, barWidth * 0.5],
|
||||
itemStyle: {
|
||||
borderWidth: 0,
|
||||
color: "#50C3E3",
|
||||
},
|
||||
},
|
||||
{
|
||||
// this bot
|
||||
z: 6,
|
||||
type: "pictorialBar",
|
||||
symbolPosition: "start",
|
||||
data: chartData.value.map((item) => item.current),
|
||||
symbol: "diamond",
|
||||
symbolOffset: ["-45%", "50%"],
|
||||
symbolSize: [barWidth, barWidth * 0.5],
|
||||
itemStyle: {
|
||||
borderWidth: 0,
|
||||
color: "#50C3E3",
|
||||
},
|
||||
},
|
||||
{
|
||||
// last top
|
||||
z: 3,
|
||||
type: "pictorialBar",
|
||||
symbolPosition: "end",
|
||||
data: chartData.value.map((item) => item.last),
|
||||
symbol: "diamond",
|
||||
symbolOffset: ["45%", "-50%"],
|
||||
symbolSize: [barWidth, barWidth * 0.5],
|
||||
itemStyle: {
|
||||
borderWidth: 0,
|
||||
color: "#FFF26D",
|
||||
},
|
||||
},
|
||||
{
|
||||
// last bot
|
||||
z: 3,
|
||||
type: "pictorialBar",
|
||||
symbolPosition: "start",
|
||||
data: chartData.value.map((item) => item.last),
|
||||
symbol: "diamond",
|
||||
symbolOffset: ["45%", "50%"],
|
||||
symbolSize: [barWidth, barWidth * 0.5],
|
||||
itemStyle: {
|
||||
borderWidth: 0,
|
||||
color: "#FFF26D",
|
||||
},
|
||||
},
|
||||
],
|
||||
tooltip: {
|
||||
trigger: "axis",
|
||||
axisPointer: { type: "shadow" },
|
||||
formatter: function (params) {
|
||||
let tooltipText = `<div>${params[0].axisValueLabel}</div>`;
|
||||
const filteredParams = params.filter((item) => item.seriesType === "bar");
|
||||
filteredParams.forEach((item) => {
|
||||
tooltipText += `<div>${item.marker} ${item.value}</div>`;
|
||||
});
|
||||
|
||||
return tooltipText;
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
async function fetchEnergyCostGrowth() {
|
||||
try {
|
||||
const res = await getSystemEnergyCostGrowth();
|
||||
energyCostGrowthData.value = res.data || {
|
||||
day: [],
|
||||
week: [],
|
||||
month: [],
|
||||
year: [],
|
||||
};
|
||||
updateChartData();
|
||||
} catch (error) {
|
||||
console.error("Error fetching energy cost growth:", error);
|
||||
energyCostGrowthData.value = { day: [], week: [], month: [], year: [] };
|
||||
chartData.value = [];
|
||||
}
|
||||
}
|
||||
|
||||
function updateChartData() {
|
||||
const list = energyCostGrowthData.value[currentType.value.name] || [];
|
||||
chartData.value = list.map((item) => ({
|
||||
name: item.name,
|
||||
current: item.current,
|
||||
last: item.last,
|
||||
difference: ((item.current ?? 0) - (item.last ?? 0)).toFixed(2),
|
||||
percentage: item.percentage,
|
||||
}));
|
||||
}
|
||||
|
||||
watch(
|
||||
() => currentType.value.name,
|
||||
(newValue) => {
|
||||
if (newValue) {
|
||||
updateChartData();
|
||||
}
|
||||
},
|
||||
{
|
||||
immediate: true,
|
||||
}
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
fetchEnergyCostGrowth();
|
||||
if (intervalId) {
|
||||
clearInterval(intervalId);
|
||||
}
|
||||
intervalId = setInterval(() => {
|
||||
fetchEnergyCostGrowth();
|
||||
}, 60 * 60 * 1000);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
clearInterval(intervalId);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-wrap">
|
||||
<div class="w-full chart-data relative px-3">
|
||||
<div class="flex flex-wrap items-center justify-between">
|
||||
<h2 class="font-light pt-1 px-1">
|
||||
{{ $t("dashboard.relative_energy_consumption") }}
|
||||
</h2>
|
||||
<Select
|
||||
:value="currentType"
|
||||
class="!w-24"
|
||||
selectClass="border-info focus-within:border-info btn-xs text-xs"
|
||||
name="name"
|
||||
Attribute="title"
|
||||
:options="energyTypeList"
|
||||
:isTopLabelExist="false"
|
||||
:isBottomLabelExist="false"
|
||||
>
|
||||
</Select>
|
||||
</div>
|
||||
<div class="h-[100px]">
|
||||
<BarChart
|
||||
id="dashboard_chart_compare"
|
||||
class="h-full"
|
||||
:option="barChartOptions"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 表格數據展示 -->
|
||||
<div class="flex justify-between">
|
||||
<div
|
||||
v-for="(data, index) in chartData"
|
||||
:key="index"
|
||||
class="text-center mx-1"
|
||||
:style="{ width: 100 / chartData.length + '%' }"
|
||||
>
|
||||
<div
|
||||
class="text-xs bg-cyan-900 p-1 border border-cyan-100 border-opacity-20"
|
||||
>
|
||||
{{ labels[0] }}
|
||||
</div>
|
||||
<div
|
||||
class="text-sm bg-cyan-900 p-1 border border-cyan-100 border-opacity-20"
|
||||
>
|
||||
{{ data.current ?? "-" }}
|
||||
</div>
|
||||
<div
|
||||
class="text-xs bg-cyan-900 p-1 border border-cyan-100 border-opacity-20"
|
||||
>
|
||||
{{ labels[1] }}
|
||||
</div>
|
||||
<div
|
||||
class="text-sm bg-cyan-900 p-1 border border-cyan-100 border-opacity-20"
|
||||
>
|
||||
{{ data.last ?? "-" }}
|
||||
</div>
|
||||
<div
|
||||
class="text-sm bg-cyan-900 p-1 border border-cyan-100 border-opacity-20"
|
||||
>
|
||||
<span
|
||||
:class="{
|
||||
'text-red-500': data.difference > 0,
|
||||
'text-green-500': data.difference < 0,
|
||||
}"
|
||||
>
|
||||
{{
|
||||
data.difference
|
||||
? (data.difference > 0 ? "+" : "") + data.difference
|
||||
: "-"
|
||||
}}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.chart-data:before {
|
||||
@apply absolute -left-0 -top-2 h-10 w-10 bg-no-repeat z-10;
|
||||
content: "";
|
||||
background: url(@ASSET/img/chart-data-background01.svg) center center;
|
||||
}
|
||||
|
||||
.chart-data::after {
|
||||
@apply absolute -right-1 -bottom-3 h-10 w-10 bg-no-repeat z-10;
|
||||
content: "";
|
||||
background: url(@ASSET/img/chart-data-background02.svg) center center;
|
||||
}
|
||||
</style>
|
140
src/views/headquarters/components/ElecRank.vue
Normal file
140
src/views/headquarters/components/ElecRank.vue
Normal file
@ -0,0 +1,140 @@
|
||||
<script setup>
|
||||
import { ref, watch, computed, onUnmounted } from "vue";
|
||||
import { getSystemEnergyCostRank } from "@/apis/headquarters";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import useBuildingStore from "@/stores/useBuildingStore";
|
||||
|
||||
const store = useBuildingStore();
|
||||
const { t } = useI18n();
|
||||
|
||||
const energyCostData = ref({});
|
||||
const energyTypeList = ref([
|
||||
{
|
||||
title: t("dashboard.today_energy_consumption"),
|
||||
key: "day",
|
||||
},
|
||||
{
|
||||
title: t("dashboard.this_month_energy_consumption"),
|
||||
key: "month",
|
||||
},
|
||||
]);
|
||||
const currentEnergyType = ref({
|
||||
name: "month",
|
||||
});
|
||||
let intervalId = null;
|
||||
|
||||
const currentEnergyData = computed(() => {
|
||||
if (!energyCostData.value) {
|
||||
return [];
|
||||
}
|
||||
return currentEnergyType.value.name === "month"
|
||||
? energyCostData.value?.month || []
|
||||
: energyCostData.value?.day || [];
|
||||
});
|
||||
|
||||
const getEnergyRank = async () => {
|
||||
try {
|
||||
const res = await getSystemEnergyCostRank({
|
||||
building_ids: store.buildings.map((building) => building.building_guid),
|
||||
});
|
||||
energyCostData.value = res.data;
|
||||
} catch (error) {
|
||||
console.error("Error fetching energy cost rank:", error);
|
||||
}
|
||||
};
|
||||
|
||||
watch(
|
||||
() => store.buildings,
|
||||
(newBuilding) => {
|
||||
if (newBuilding) {
|
||||
getEnergyRank();
|
||||
|
||||
if (intervalId) {
|
||||
clearInterval(intervalId);
|
||||
}
|
||||
intervalId = setInterval(() => {
|
||||
getEnergyRank();
|
||||
}, 60 * 60 * 1000);
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
onUnmounted(() => {
|
||||
clearInterval(intervalId);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="state-box-col relative">
|
||||
<!-- 標題和切換按鈕 -->
|
||||
<div class="flex justify-between items-center mb-2">
|
||||
<h2 class="font-light relative">
|
||||
{{ $t("dashboard.energy_ranking") }}
|
||||
</h2>
|
||||
<Select
|
||||
:value="currentEnergyType"
|
||||
class="!w-24"
|
||||
selectClass="border-info focus-within:border-info btn-xs text-xs"
|
||||
name="name"
|
||||
Attribute="title"
|
||||
:options="energyTypeList"
|
||||
:isTopLabelExist="false"
|
||||
:isBottomLabelExist="false"
|
||||
>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<!-- 能耗排名列表 -->
|
||||
<div class="overflow-y-auto" style="height: 200px;">
|
||||
<table class="table table-sm text-center">
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="(item, index) in currentEnergyData"
|
||||
:key="index"
|
||||
:class="[
|
||||
{ 'text-red-300': index + 1 === 1 },
|
||||
{ 'text-orange-300': index + 1 === 2 },
|
||||
{ 'text-yellow-300': index + 1 === 3 },
|
||||
{ 'text-teal-300': index + 1 > 3 },
|
||||
]"
|
||||
>
|
||||
<td class="px-0 align-top">
|
||||
<p class="flex items-center">
|
||||
<font-awesome-icon :icon="['fas', 'crown']" class="me-1" />
|
||||
{{ index + 1 }}
|
||||
</p>
|
||||
</td>
|
||||
<td class="align-top whitespace-nowrap px-0">
|
||||
{{ item.site_name }}
|
||||
</td>
|
||||
<td class="align-top">{{ item.name }}</td>
|
||||
<td class="align-top ps-0">{{ item.value }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.state-box-col {
|
||||
@apply border-2 border-light-info rounded-sm p-2 text-white relative;
|
||||
}
|
||||
|
||||
.state-box-col:before {
|
||||
@apply absolute left-0 right-0 -top-0.5 m-auto h-2 w-36 bg-no-repeat bg-center z-10;
|
||||
content: "";
|
||||
background-image: url(@ASSET/img/state-box-top.png);
|
||||
}
|
||||
|
||||
.state-box-col:after {
|
||||
@apply absolute left-0 right-0 -bottom-0.5 m-auto h-2 w-36 bg-no-repeat bg-center z-10;
|
||||
content: "";
|
||||
background-image: url(@ASSET/img/state-box-bottom.png);
|
||||
}
|
||||
|
||||
tr td {
|
||||
@apply text-[13px] text-start;
|
||||
}
|
||||
</style>
|
208
src/views/headquarters/components/ElecTrends.vue
Normal file
208
src/views/headquarters/components/ElecTrends.vue
Normal file
@ -0,0 +1,208 @@
|
||||
<script setup>
|
||||
import { ref, onMounted, watch, onUnmounted } from "vue";
|
||||
import * as echarts from "echarts";
|
||||
import { getSystemEnergyCostTrend } from "@/apis/headquarters";
|
||||
import BarChart from "@/components/chart/BarChart.vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import dayjs from "dayjs";
|
||||
import useBuildingStore from "@/stores/useBuildingStore";
|
||||
|
||||
const storeBuild = useBuildingStore();
|
||||
const { t } = useI18n();
|
||||
|
||||
const chartData = ref([]);
|
||||
const buildingList = ref([]);
|
||||
const energyCostData = ref([]);
|
||||
const weekComparisonOption = ref({});
|
||||
const currentType = ref({});
|
||||
let intervalId = null;
|
||||
// 生成柱狀圖的 option
|
||||
const generateCylinderChartOption = (data) => {
|
||||
const barWidth = 15;
|
||||
return {
|
||||
xAxis: {
|
||||
type: "category",
|
||||
data: data.map((item) => item.date),
|
||||
axisLine: {
|
||||
lineStyle: {
|
||||
color: "#fff",
|
||||
},
|
||||
},
|
||||
},
|
||||
yAxis: {
|
||||
type: "value",
|
||||
name: "kWh",
|
||||
axisLine: {
|
||||
lineStyle: {
|
||||
color: "#fff",
|
||||
},
|
||||
},
|
||||
splitLine: {
|
||||
show: false,
|
||||
},
|
||||
},
|
||||
series: [
|
||||
{
|
||||
data: data.map((item) => item.energy),
|
||||
type: "bar",
|
||||
barWidth: barWidth,
|
||||
itemStyle: {
|
||||
color: new echarts.graphic.LinearGradient(0, 0, 1, 1, [
|
||||
{ offset: 0, color: "#1F7B47" },
|
||||
{ offset: 1, color: "#247E95" },
|
||||
]),
|
||||
shadowBlur: 5,
|
||||
shadowColor: "rgba(0, 0, 0, 0.5)",
|
||||
shadowOffsetY: 5,
|
||||
},
|
||||
},
|
||||
{
|
||||
z: 15,
|
||||
type: "pictorialBar",
|
||||
symbolPosition: "end",
|
||||
data: data.map((item) => item.energy),
|
||||
symbol: "diamond",
|
||||
symbolOffset: [0, -5],
|
||||
symbolSize: [barWidth, barWidth * 0.5],
|
||||
itemStyle: {
|
||||
color: "#62E39A",
|
||||
},
|
||||
},
|
||||
{
|
||||
z: 10,
|
||||
type: "pictorialBar",
|
||||
data: data.map((item) => item.energy),
|
||||
symbol: "diamond",
|
||||
symbolSize: [barWidth, barWidth * 0.5],
|
||||
symbolOffset: [0, 6],
|
||||
itemStyle: {
|
||||
color: "#247E95",
|
||||
},
|
||||
},
|
||||
],
|
||||
grid: {
|
||||
left: "0%",
|
||||
right: "0%",
|
||||
bottom: "0%",
|
||||
top: "16%",
|
||||
containLabel: true,
|
||||
},
|
||||
tooltip: {
|
||||
trigger: "axis",
|
||||
formatter: function (params) {
|
||||
const item = params[0];
|
||||
return `<p>${item.name}</p> <p>${item.marker}Energy consumption : ${item.value}</p>`;
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const processEnergyData = async () => {
|
||||
try {
|
||||
const res = await getSystemEnergyCostTrend({
|
||||
building_id: currentType.value.name,
|
||||
});
|
||||
energyCostData.value = res.data.trend || [];
|
||||
if (!energyCostData.value || energyCostData.value.length === 0) {
|
||||
chartData.value = [];
|
||||
weekComparisonOption.value = generateCylinderChartOption(chartData.value);
|
||||
return;
|
||||
}
|
||||
const dailyData = [...energyCostData.value].sort(
|
||||
(a, b) => new Date(a.time) - new Date(b.time)
|
||||
);
|
||||
chartData.value = dailyData.map((item) => ({
|
||||
date: dayjs(item.time).format("MM/DD"),
|
||||
energy: item.value,
|
||||
}));
|
||||
weekComparisonOption.value = generateCylinderChartOption(chartData.value);
|
||||
} catch (error) {
|
||||
console.error("Error fetching energy cost trend:", error);
|
||||
}
|
||||
};
|
||||
|
||||
watch(
|
||||
() => storeBuild.buildings,
|
||||
(newValue) => {
|
||||
if (newValue) {
|
||||
currentType.value = {
|
||||
name: newValue[0]?.building_guid || "all",
|
||||
};
|
||||
buildingList.value = [
|
||||
...newValue.map((building) => ({
|
||||
title: building.full_name,
|
||||
key: building.building_guid,
|
||||
})),
|
||||
];
|
||||
}
|
||||
},
|
||||
{
|
||||
immediate: true,
|
||||
}
|
||||
);
|
||||
|
||||
// 監聽 currentType 變化時重新取得資料
|
||||
watch(
|
||||
() => currentType.value.name,
|
||||
(newValue) => {
|
||||
if (newValue) {
|
||||
processEnergyData();
|
||||
if (intervalId) {
|
||||
clearInterval(intervalId);
|
||||
}
|
||||
intervalId = setInterval(() => {
|
||||
processEnergyData();
|
||||
}, 60 * 60 * 1000);
|
||||
}
|
||||
},
|
||||
{
|
||||
immediate: true,
|
||||
}
|
||||
);
|
||||
|
||||
onUnmounted(() => {
|
||||
clearInterval(intervalId);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="w-full chart-data relative px-3">
|
||||
<div class="flex flex-wrap items-center justify-between">
|
||||
<h2 class="font-light pt-1 px-1">
|
||||
{{ $t("dashboard.last_30_days_energy_trend") }}
|
||||
</h2>
|
||||
<Select
|
||||
:value="currentType"
|
||||
class="w-[8.5rem] my-2"
|
||||
selectClass="border-info focus-within:border-info btn-xs text-xs"
|
||||
name="name"
|
||||
Attribute="title"
|
||||
:options="buildingList"
|
||||
:isTopLabelExist="false"
|
||||
:isBottomLabelExist="false"
|
||||
>
|
||||
</Select>
|
||||
</div>
|
||||
<div class="h-[200px]">
|
||||
<BarChart
|
||||
id="dashboard_chart_week_comparison"
|
||||
class="h-full"
|
||||
:option="weekComparisonOption"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.chart-data:before {
|
||||
@apply absolute -left-0 -top-1 h-10 w-10 bg-no-repeat z-10;
|
||||
content: "";
|
||||
background: url(@ASSET/img/chart-data-background01.svg) center center;
|
||||
}
|
||||
|
||||
.chart-data::after {
|
||||
@apply absolute -right-1 bottom-1 h-10 w-10 bg-no-repeat z-10;
|
||||
content: "";
|
||||
background: url(@ASSET/img/chart-data-background02.svg) center center;
|
||||
}
|
||||
</style>
|
139
src/views/headquarters/components/SysMap.vue
Normal file
139
src/views/headquarters/components/SysMap.vue
Normal file
@ -0,0 +1,139 @@
|
||||
<script setup>
|
||||
import L from "leaflet";
|
||||
import "leaflet/dist/leaflet.css";
|
||||
import { onMounted, ref } from "vue";
|
||||
import { nextTick } from "vue";
|
||||
|
||||
const leafletmapContainer = ref(null);
|
||||
const selectedFactory = ref("");
|
||||
let map = null;
|
||||
let markerRefs = [];
|
||||
|
||||
const customOptions = {
|
||||
minWidth: 250,
|
||||
};
|
||||
|
||||
const markers = [
|
||||
{
|
||||
position: [31.29834, 120.58319],
|
||||
popup: {
|
||||
title: "CCT瀚荃蘇州廠",
|
||||
img: "https://picsum.photos/id/700/600/400",
|
||||
deviceNumber: 10,
|
||||
online: 8,
|
||||
offline: 2,
|
||||
},
|
||||
},
|
||||
{
|
||||
position: [29.56301, 106.55156],
|
||||
popup: {
|
||||
title: "CCT瀚荃重慶廠",
|
||||
img: "https://picsum.photos/id/701/600/400",
|
||||
deviceNumber: 20,
|
||||
online: 15,
|
||||
offline: 5,
|
||||
},
|
||||
},
|
||||
{
|
||||
position: [23.02067, 113.75179],
|
||||
popup: {
|
||||
title: "CCT瀚荃東莞廠",
|
||||
img: "https://picsum.photos/id/702/600/400",
|
||||
deviceNumber: 30,
|
||||
online: 25,
|
||||
offline: 5,
|
||||
},
|
||||
},
|
||||
{
|
||||
position: [25.16742, 121.44587],
|
||||
popup: {
|
||||
title: "CCT瀚荃淡水廠",
|
||||
img: "https://picsum.photos/id/703/600/400",
|
||||
deviceNumber: 46,
|
||||
online: 25,
|
||||
offline: 21,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
onMounted(() => {
|
||||
map = L.map(leafletmapContainer.value, {
|
||||
center: [31.35, 113.4],
|
||||
zoom: 5,
|
||||
});
|
||||
|
||||
L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
|
||||
attribution:
|
||||
'© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
|
||||
}).addTo(map);
|
||||
|
||||
markerRefs = markers.map(({ position, popup }) => {
|
||||
const marker = L.marker(position)
|
||||
.bindPopup(
|
||||
`
|
||||
<div class="font-bold text-lg mb-2">${popup.title}</div>
|
||||
<img src="${popup.img}" class="w-full rounded mb-2" />
|
||||
<div class="flex justify-between text-base mt-2">
|
||||
<div class="text-center">
|
||||
設備總數<br><span class="text-white text-2xl">${popup.deviceNumber}</span>
|
||||
</div>
|
||||
<div class="text-center">
|
||||
在線設備<br><span class="text-green-500 text-2xl">${popup.online}</span>
|
||||
</div>
|
||||
<div class="text-center">
|
||||
離線設備<br><span class="text-red-600 text-2xl">${popup.offline}</span>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
customOptions
|
||||
)
|
||||
.addTo(map);
|
||||
return marker;
|
||||
});
|
||||
});
|
||||
|
||||
function focusFactory(idx) {
|
||||
if (!map || !markerRefs[idx]) return;
|
||||
const pos = markers[idx].position;
|
||||
map.flyTo(pos, 6, { animate: true });
|
||||
markerRefs[idx].openPopup();
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<div class="relative w-full h-full">
|
||||
<div class="absolute top-4 right-4 z-20 flex items-center gap-2">
|
||||
<select
|
||||
id="factory-select"
|
||||
v-model="selectedFactory"
|
||||
class="select select-sm bg-cyan-950 rounded-md border-info focus-within:border-info"
|
||||
@change="focusFactory(selectedFactory)"
|
||||
>
|
||||
<option value="" disabled>下屬共計 5 家子企業</option>
|
||||
<option v-for="(m, idx) in markers" :key="idx" :value="idx">
|
||||
{{ m.popup.title }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="leafletmapContainer z-10" ref="leafletmapContainer"></div>
|
||||
</div>
|
||||
</template>
|
||||
<style lang="scss">
|
||||
.leafletmapContainer {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.leaflet-popup-content-wrapper,
|
||||
.leaflet-popup-tip {
|
||||
background: #164e63 !important; // 你要的顏色
|
||||
color: #ffffff;
|
||||
box-shadow: 0 3px 14px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
.leaflet-container a.leaflet-popup-close-button{
|
||||
color: #fff;
|
||||
font-size: 1rem;
|
||||
padding-top: 0.4rem;
|
||||
}
|
||||
</style>
|
151
src/views/headquarters/components/SysProgress.vue
Normal file
151
src/views/headquarters/components/SysProgress.vue
Normal file
@ -0,0 +1,151 @@
|
||||
<script setup>
|
||||
import { ref, computed, watch, onUnmounted } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import { getSystemStatus } from "@/apis/headquarters";
|
||||
import useBuildingStore from "@/stores/useBuildingStore";
|
||||
import SysProgressModal from "./SysProgressModal.vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
const { t } = useI18n();
|
||||
const router = useRouter();
|
||||
const store = useBuildingStore();
|
||||
const equipmentData = ref([]);
|
||||
const modalData = ref({});
|
||||
let intervalId = null;
|
||||
|
||||
const openModal = (item) => {
|
||||
modalData.value = item;
|
||||
system_status_modal.showModal();
|
||||
};
|
||||
|
||||
const onCancel = () => {
|
||||
modalData.value = {};
|
||||
system_status_modal.close();
|
||||
};
|
||||
|
||||
const getAlarmsInfos = async () => {
|
||||
try {
|
||||
const res = await getSystemStatus({
|
||||
building_ids: store.buildings.map((building) => building.building_guid),
|
||||
});
|
||||
const apiData = res.data;
|
||||
|
||||
// 轉換 equipmentData 的資料格式
|
||||
if (apiData && apiData.alarm) {
|
||||
equipmentData.value = apiData.alarm.map((item) => ({
|
||||
label: item.system_name,
|
||||
online: item.online || 0,
|
||||
offline: item.offline || 0,
|
||||
alarm: item.alarm || 0,
|
||||
}));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching alarm info:", error);
|
||||
}
|
||||
};
|
||||
|
||||
watch(
|
||||
() => store.buildings,
|
||||
(newBuilding) => {
|
||||
if (newBuilding) {
|
||||
getAlarmsInfos();
|
||||
|
||||
if (intervalId) {
|
||||
clearInterval(intervalId);
|
||||
}
|
||||
intervalId = setInterval(() => {
|
||||
getAlarmsInfos();
|
||||
}, 30000);
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
onUnmounted(() => {
|
||||
clearInterval(intervalId);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SysProgressModal :onCancel="onCancel" :modalData="modalData" />
|
||||
<div class="w-full state-box-col relative">
|
||||
<div class="state-box">
|
||||
<div class="title">
|
||||
<img class="state-title01" src="@ASSET/img/state-title01.svg" />
|
||||
<span class="">{{$t("dashboard.system_status")}}</span>
|
||||
<img class="state-title02" src="@ASSET/img/state-title02.svg" />
|
||||
</div>
|
||||
<table class="table table-sm text-center">
|
||||
<thead>
|
||||
<tr class="border-cyan-400 text-cyan-100">
|
||||
<th></th>
|
||||
<th>{{ $t("alert.online") }}</th>
|
||||
<th>{{ $t("alert.offline") }}</th>
|
||||
<th>{{ $t("alert.alarm") }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="(item, index) in equipmentData"
|
||||
:key="index"
|
||||
class="border-cyan-400 cursor-pointer hover:text-info"
|
||||
@click.stop.prevent="openModal(item)"
|
||||
>
|
||||
<th class="px-0 text-start">{{ item.label }}</th>
|
||||
<td>
|
||||
{{ item.online.length }}
|
||||
</td>
|
||||
<td>
|
||||
{{ item.offline.length }}
|
||||
</td>
|
||||
<td>
|
||||
{{ item.alarm.length }}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.state-box-col:before {
|
||||
@apply absolute left-0 right-0 -top-0.5 m-auto h-2 w-36 bg-no-repeat bg-center z-10;
|
||||
content: "";
|
||||
background-image: url(@ASSET/img/state-box-top.png);
|
||||
}
|
||||
|
||||
.state-box-col:after {
|
||||
@apply absolute left-0 right-0 -bottom-0.5 m-auto h-2 w-36 bg-no-repeat bg-center z-10;
|
||||
content: "";
|
||||
background-image: url(@ASSET/img/state-box-bottom.png);
|
||||
}
|
||||
|
||||
.state-box-col {
|
||||
@apply border border-light-info shadow-md shadow-blue-300 rounded-sm py-2 px-6 text-white relative;
|
||||
}
|
||||
|
||||
.state-box:after {
|
||||
@apply absolute right-3 top-3 w-4 h-4 bg-no-repeat bg-center z-10;
|
||||
content: "";
|
||||
background-image: url(@ASSET/img/state-title01.svg);
|
||||
}
|
||||
|
||||
.state-box:before {
|
||||
@apply absolute right-0.5 bottom-5 w-4 h-32 bg-no-repeat bg-center z-10;
|
||||
content: "";
|
||||
background-image: url(@ASSET/img/state-ul-background02.svg);
|
||||
}
|
||||
|
||||
.state-box .title {
|
||||
@apply relative flex items-center mb-1;
|
||||
}
|
||||
|
||||
.state-box .title .state-title01 {
|
||||
@apply w-4 mr-1.5;
|
||||
}
|
||||
|
||||
.state-box .title .state-title02 {
|
||||
@apply w-5 ml-1.5;
|
||||
}
|
||||
</style>
|
132
src/views/headquarters/components/SysProgressModal.vue
Normal file
132
src/views/headquarters/components/SysProgressModal.vue
Normal file
@ -0,0 +1,132 @@
|
||||
<script setup>
|
||||
import { ref, onMounted, defineProps, inject, watch } from "vue";
|
||||
import useActiveBtn from "@/hooks/useActiveBtn";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
const { t } = useI18n();
|
||||
const props = defineProps({
|
||||
onCancel: Function,
|
||||
modalData: Object,
|
||||
});
|
||||
const { items, changeActiveBtn, setItems, selectedBtn } = useActiveBtn();
|
||||
const detailData = ref([]);
|
||||
onMounted(() => {
|
||||
setItems([
|
||||
{
|
||||
title: t("alert.online"),
|
||||
key: "online",
|
||||
active: true,
|
||||
},
|
||||
{
|
||||
title: t("alert.offline"),
|
||||
key: "offline",
|
||||
active: false,
|
||||
},
|
||||
{
|
||||
title: t("alert.alarm"),
|
||||
key: "alarm",
|
||||
active: false,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
watch(selectedBtn, (newVal, oldVal) => {
|
||||
if (newVal) {
|
||||
detailData.value = props.modalData[newVal.key];
|
||||
}
|
||||
});
|
||||
|
||||
watch(
|
||||
() => props.modalData,
|
||||
(newVal, oldVal) => {
|
||||
if (newVal) {
|
||||
changeActiveBtn(items.value[0]);
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal id="system_status_modal" :onCancel="onCancel" :width="600">
|
||||
<template #modalTitle>
|
||||
<div class="flex items-center justify-between">
|
||||
<ButtonGroup
|
||||
:items="items"
|
||||
:withLine="true"
|
||||
className="btn-sm"
|
||||
:onclick="
|
||||
(e, item) => {
|
||||
changeActiveBtn(item);
|
||||
}
|
||||
"
|
||||
/>
|
||||
<button
|
||||
type="link"
|
||||
class="btn-link btn-text-without-border px-2"
|
||||
@click="onCancel"
|
||||
>
|
||||
<font-awesome-icon :icon="['fas', 'times']" class="text-[#a5abb1]" />
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
<template #modalContent>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="table text-base mt-5">
|
||||
<thead>
|
||||
<tr>
|
||||
<th
|
||||
class="text-base border text-white text-center bg-cyan-600 bg-opacity-30"
|
||||
>
|
||||
{{ $t("table.serial_number") }}
|
||||
</th>
|
||||
<th
|
||||
class="text-base border text-white text-center bg-cyan-600 bg-opacity-30"
|
||||
>
|
||||
{{ $t("history.building_name") }}
|
||||
</th>
|
||||
<th
|
||||
class="text-base border text-white text-center bg-cyan-600 bg-opacity-30"
|
||||
>
|
||||
{{ $t("table.name") }}
|
||||
</th>
|
||||
<th
|
||||
class="text-base border text-white text-center bg-cyan-600 bg-opacity-30"
|
||||
>
|
||||
{{ $t("operation.updated_time") }}
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
v-if="detailData?.length > 0"
|
||||
v-for="(equipment, index) in detailData"
|
||||
:key="index"
|
||||
class="hover:bg-gray-700"
|
||||
>
|
||||
<td class="border text-white text-center">
|
||||
{{ index + 1 }}
|
||||
</td>
|
||||
<td class="border text-white text-center">
|
||||
{{ equipment.building_name }}
|
||||
</td>
|
||||
<td class="border text-white text-center">
|
||||
{{ equipment.name }}
|
||||
</td>
|
||||
<td class="border text-white text-center">
|
||||
{{ equipment.time || "-" }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-else>
|
||||
<td colspan="4" class="border text-white text-center">
|
||||
{{ $t("table.no_data") }}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</template>
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped></style>
|
@ -37,8 +37,15 @@ const doLogin = async () => {
|
||||
const res = await Login(value);
|
||||
if (res.isSuccess) {
|
||||
store.user = res.data;
|
||||
localStorage.setItem("CviBuildingList", JSON.stringify(res.data.buildingIdList));
|
||||
localStorage.setItem(
|
||||
"CviBuildingList",
|
||||
JSON.stringify(res.data.building_infos)
|
||||
);
|
||||
if (res.data.building_infos.map((b) => b.is_headquarter).includes(true)) {
|
||||
router.replace({ path: "/headquarters" });
|
||||
} else {
|
||||
router.replace({ path: "/dashboard" });
|
||||
}
|
||||
} else {
|
||||
openToast("error", res.msg);
|
||||
}
|
||||
@ -47,7 +54,7 @@ const doLogin = async () => {
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="absolute top-10 left-1/2 -translate-x-1/2 z-20 border-4 rounded-3xl p-5 bg-white bg-opacity-60"
|
||||
class="absolute top-10 left-1/2 -translate-x-1/2 z-20 border-2 border-slate-200/40 shadow-lg shadow-slate-100/20 rounded-3xl p-5 bg-gradient-to-br from-slate-600 to-slate-800 bg-opacity-60"
|
||||
>
|
||||
<div class="flex flex-col items-start w-96">
|
||||
<div
|
||||
@ -58,7 +65,7 @@ const doLogin = async () => {
|
||||
</div>
|
||||
<div class="w-full flex flex-col items-end my-2">
|
||||
<div class="w-full flex justify-between">
|
||||
<label class="mr-2 text-2xl text-black" for="account">{{
|
||||
<label class="mr-2 text-2xl text-slate-200" for="account">{{
|
||||
$t("account")
|
||||
}}</label>
|
||||
<input
|
||||
@ -74,7 +81,7 @@ const doLogin = async () => {
|
||||
</div>
|
||||
<div class="w-full flex flex-col items-end my-2">
|
||||
<div class="w-full flex justify-between relative">
|
||||
<label class="mr-2 text-2xl text-black" for="password">{{
|
||||
<label class="mr-2 text-2xl text-slate-200" for="password">{{
|
||||
$t("password")
|
||||
}}</label>
|
||||
<input
|
||||
|
@ -130,11 +130,6 @@ watch(
|
||||
(newBuilding) => {
|
||||
if (Boolean(newBuilding)) {
|
||||
getData();
|
||||
imgBaseUrl.value = buildingStore.previewImageExt
|
||||
? `${FILE_BASEURL}/upload/setting/previewImage/${newBuilding.building_guid}${buildingStore.previewImageExt}`
|
||||
: import.meta.env.MODE === "production"
|
||||
? "dist/build_img.jpg"
|
||||
: "/build_img.jpg";
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -143,6 +138,18 @@ watch(
|
||||
}
|
||||
);
|
||||
|
||||
watch(
|
||||
[() => buildingStore.previewImageExt, () => buildingStore.selectedBuilding?.building_guid],
|
||||
([newExt, buildingGuid]) => {
|
||||
if (newExt && buildingGuid) {
|
||||
imgBaseUrl.value = `${FILE_BASEURL}/upload/setting/previewImage/${buildingGuid}${newExt}`;
|
||||
} else {
|
||||
imgBaseUrl.value = `${FILE_BASEURL}/upload/build_img.jpg`;
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
watch(
|
||||
() => deptData.value,
|
||||
(newVal) => {
|
||||
@ -184,7 +191,7 @@ const getAllDeviceRealtime = async () => {
|
||||
const res = await getSystemRealTime(
|
||||
subscribeData.value.map((d) => d.device_number)
|
||||
);
|
||||
console.log(res.data);
|
||||
console.log("realtimeData",res.data);
|
||||
realtimeData.value = res.data;
|
||||
};
|
||||
await fetchData(); // 立即執行一次
|
||||
@ -262,13 +269,13 @@ const getCurrentInfoModalData = async (e, position, value) => {
|
||||
document.getElementById("system_info_modal").showModal();
|
||||
};
|
||||
|
||||
const selectedDeviceRealtime = computed(
|
||||
() =>
|
||||
realtimeData.value?.find(
|
||||
({ device_number }) =>
|
||||
device_number === selectedDevice.value?.value?.device_number
|
||||
)?.data
|
||||
);
|
||||
const selectedDeviceRealtime = computed(() => {
|
||||
const deviceNumber = selectedDevice.value?.value?.device_number;
|
||||
if (!deviceNumber) return [];
|
||||
return realtimeData.value
|
||||
.filter(item => item.device_number === deviceNumber && Array.isArray(item.data))
|
||||
.flatMap(item => item.data.map(dataItem => ({ ...dataItem, topic_publish: item.topic_publish })));
|
||||
});
|
||||
|
||||
const clearSelectedDeviceInfo = () => {
|
||||
selectedDevice.value.value = null;
|
||||
|
@ -25,8 +25,8 @@ const getDeviceStatusColor = (device) => {
|
||||
if (device.full_name === 'SmartSocket-AA001') return 'red';
|
||||
if (device.full_name === 'SmartSocket-AA003' || device.full_name === 'SmartSocket-AA004') return 'gray';
|
||||
const state = getDeviceRealtimeState(device.device_number);
|
||||
if (state === 'offnormal' || state === '') return device.device_close_color || 'gray';
|
||||
return device.device_normal_color;
|
||||
if (state === 'offnormal' || state === '') return device.device_close_color || '#9D9D9D';
|
||||
return device.device_normal_color || '#009100';
|
||||
};
|
||||
|
||||
// 狀態文字
|
||||
|
@ -1,8 +1,9 @@
|
||||
<script setup>
|
||||
import { ref, computed, inject, watch } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { toggleDevicePower } from "@/apis/system";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
const { openToast } = inject("app_toast");
|
||||
const { t } = useI18n();
|
||||
const { selectedDevice, selectedDeviceRealtime } = inject(
|
||||
"system_selectedDevice"
|
||||
@ -13,7 +14,7 @@ const groupedData = computed(() => {
|
||||
|
||||
if (selectedDeviceRealtime?.value) {
|
||||
selectedDeviceRealtime.value.forEach((record) => {
|
||||
const { ori_device_number, time } = record;
|
||||
const { ori_device_number, time, topic_publish } = record;
|
||||
|
||||
if (!grouped[ori_device_number]) {
|
||||
grouped[ori_device_number] = {
|
||||
@ -26,6 +27,8 @@ const groupedData = computed(() => {
|
||||
if (record.point === d.points) {
|
||||
grouped[ori_device_number].data.push({
|
||||
...d,
|
||||
device_item_id: record.device_item_id,
|
||||
topic_publish: record.topic_publish,
|
||||
value: record.value,
|
||||
});
|
||||
}
|
||||
@ -36,9 +39,19 @@ const groupedData = computed(() => {
|
||||
return grouped;
|
||||
});
|
||||
|
||||
const togglePowerSwitch = (e, val) => {
|
||||
const togglePowerSwitch = async (e, device_item_id, topic_publish) => {
|
||||
const isChecked = e.target.checked;
|
||||
console.log("Power Switch", e, val, "狀態:", isChecked);
|
||||
const res = await toggleDevicePower({
|
||||
device_item_id,
|
||||
topic_publish,
|
||||
new_value: isChecked,
|
||||
});
|
||||
|
||||
if (res.isSuccess) {
|
||||
openToast("success", t("msg.edit_successfully"), "#system_info_modal");
|
||||
} else {
|
||||
openToast("error", res.msg, "#system_info_modal");
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
@ -64,21 +77,23 @@ const togglePowerSwitch = (e, val) => {
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(group, index) in group.data" :key="index">
|
||||
<tr v-for="(items, index) in group.data" :key="index">
|
||||
<td class="border text-white text-lg text-center">
|
||||
{{ group.full_name }}
|
||||
{{ items.full_name }}
|
||||
</td>
|
||||
<td class="border text-white text-lg text-center">
|
||||
<template v-if="group.full_name === 'Power Switch'">
|
||||
<template v-if="items.full_name === 'Power Switch'">
|
||||
<input
|
||||
type="checkbox"
|
||||
class="toggle toggle-success"
|
||||
:checked="group.value"
|
||||
@change="togglePowerSwitch($event, group.value)"
|
||||
:checked="items.value"
|
||||
@change="
|
||||
togglePowerSwitch($event, items.device_item_id, items.topic_publish)
|
||||
"
|
||||
/>
|
||||
</template>
|
||||
<template v-else>
|
||||
{{ group.value }}
|
||||
{{ items.value }}
|
||||
</template>
|
||||
</td>
|
||||
</tr>
|
||||
|
Loading…
Reference in New Issue
Block a user