Merge branch 'feature/headquartersSetting' into feature/dockerSetting
This commit is contained in:
commit
42742d1063
29
.github/prompts/exportCSV.prompt.md
vendored
Normal file
29
.github/prompts/exportCSV.prompt.md
vendored
Normal file
@ -0,0 +1,29 @@
|
||||
---
|
||||
mode: agent
|
||||
---
|
||||
# API 路徑整理與引用檢查規則
|
||||
|
||||
## 目標
|
||||
- 針對 apis 目錄下所有子資料夾(如 account、alert、asset、building、dashboard、energy、forge、graph、history、login、operation、productSetting、system)的 api.js 與 index.js 檔案,完整追蹤 API 路徑的實際引用情形。
|
||||
- 追蹤流程:
|
||||
1. 先在 api.js 找出所有 API 路徑常數(如 `export const GET_XXX_API = '/path'`)。
|
||||
2. 在 index.js 檔案確認這些常數有被 import 並包裝成 API function(如 `getXXX`)。
|
||||
3. 再全專案搜尋這些 API function 是否有被其他檔案 import 並呼叫。
|
||||
- 產生一份 csv 報表,格式如下:
|
||||
|
||||
| API 路徑 | 定義常數 | API function | 是否有被引用 |
|
||||
|----------|----------|--------------|-------------|
|
||||
| /user | GET_USER_API | getUser | Y |
|
||||
| /admin | GET_ADMIN_API | getAdmin | N |
|
||||
|
||||
## 詳細規則
|
||||
- 處理 apis 目錄下所有子資料夾的 api.js 與 index.js 檔案。
|
||||
- API 路徑的定義需涵蓋 get/post/put/delete 等(如 `export const API = '/path'`)。
|
||||
- 只統計有被 index.js import 並包裝成 function 的 API 路徑。
|
||||
- 檢查 function 是否有被其他檔案 import 並呼叫(排除 apis 目錄本身)。
|
||||
- 匹配到的檔案需記錄完整路徑,可多個檔案以分號分隔。
|
||||
- 統計結果輸出為 csv 檔案。
|
||||
|
||||
## 輸出
|
||||
- 檔名:api_usage_report.csv
|
||||
- 欄位:API 路徑, 定義常數, API function, 是否有被引用,
|
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",
|
||||
|
@ -6,7 +6,8 @@
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
"preview": "vite preview",
|
||||
"build:staging": "vite build --mode staging"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ant-design/icons-vue": "^7.0.1",
|
||||
@ -23,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 |
@ -34,4 +34,6 @@ 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`;
|
||||
export const POST_ASSET_ELEC_SETTING_API = `/AssetManage/SaveAssetSetting`;
|
||||
|
||||
export const POST_ASSET_MQTT_PUBLISH_API = `/api/mqtt/publish`;
|
@ -26,13 +26,14 @@ import {
|
||||
POST_ASSET_ELECTYPE_API,
|
||||
DELETE_ASSET_ELECTYPE_API,
|
||||
POST_ASSET_ELEC_SETTING_API,
|
||||
POST_ASSET_MQTT_PUBLISH_API,
|
||||
} from "./api";
|
||||
import instance from "@/util/request";
|
||||
import apihandler from "@/util/apiHandler";
|
||||
import { object } from "yup";
|
||||
|
||||
export const getAssetMainList = async (building_guid) => {
|
||||
const res = await instance.post(GET_ASSET_MAIN_LIST_API,{building_guid});
|
||||
const res = await instance.post(GET_ASSET_MAIN_LIST_API, { building_guid });
|
||||
|
||||
return apihandler(res.code, res.data, {
|
||||
msg: res.msg,
|
||||
@ -49,12 +50,17 @@ export const deleteAssetMainItem = async (id) => {
|
||||
});
|
||||
};
|
||||
|
||||
export const postAssetMainList = async ({ id, system_key, system_value, building_guid }) => {
|
||||
export const postAssetMainList = async ({
|
||||
id,
|
||||
system_key,
|
||||
system_value,
|
||||
building_guid,
|
||||
}) => {
|
||||
const res = await instance.post(POST_ASSET_MAIN_LIST_API, {
|
||||
id,
|
||||
system_key,
|
||||
system_value,
|
||||
building_guid
|
||||
building_guid,
|
||||
});
|
||||
|
||||
return apihandler(res.code, res.data, {
|
||||
@ -241,6 +247,9 @@ export const postDeviceItem = async ({
|
||||
decimals,
|
||||
is_bool,
|
||||
is_link,
|
||||
show_event_switch_btn,
|
||||
event_switch_on_message,
|
||||
event_switch_off_message,
|
||||
}) => {
|
||||
const res = await instance.post(POST_ASSET_DEVICE_ITEM_API, {
|
||||
id,
|
||||
@ -250,6 +259,9 @@ export const postDeviceItem = async ({
|
||||
decimals,
|
||||
is_bool,
|
||||
is_link,
|
||||
show_event_switch_btn,
|
||||
event_switch_on_message,
|
||||
event_switch_off_message,
|
||||
});
|
||||
|
||||
return apihandler(res.code, res.data, {
|
||||
@ -335,3 +347,15 @@ export const postAssetElecSetting = async (formData) => {
|
||||
code: res.code,
|
||||
});
|
||||
};
|
||||
|
||||
export const postMQTTpublish = async ({ Topic, Payload }) => {
|
||||
const res = await instance.post(POST_ASSET_MQTT_PUBLISH_API, {
|
||||
Topic,
|
||||
Payload,
|
||||
});
|
||||
|
||||
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,
|
||||
@ -50,7 +51,7 @@ export const getAuth = async (lang) => {
|
||||
};
|
||||
|
||||
export const getAllSysSidebar = async (building_guid) => {
|
||||
const res = await instance.post(GET_SUBAUTHPAGE_API, {building_guid});
|
||||
const res = await instance.post(GET_SUBAUTHPAGE_API, { building_guid });
|
||||
return apihandler(res.code, res.data, {
|
||||
msg: res.msg,
|
||||
code: res.code,
|
||||
|
@ -10,4 +10,8 @@ export const GET_DASHBOARD_PRODUCT_HISTORY_API = `/SituationRoom/GetProductionHi
|
||||
|
||||
export const GET_DASHBOARD_ENERGY_INFO_API = `api/dashboard/GetEnergyInfo`
|
||||
export const GET_DASHBOARD_ENERGY_COST_API = `api/dashboard/GetEnergyCost`
|
||||
export const GET_DASHBOARD_ALARMOPERATION_INFO_API = `api/dashboard/GetAlarmOperationInfo`
|
||||
export const GET_DASHBOARD_ALARMOPERATION_INFO_API = `api/dashboard/GetAlarmOperationInfo`
|
||||
|
||||
export const GET_DASHBOARD_2D3DINFO_API = `api/setting/visual/query`
|
||||
export const POST_DASHBOARD_2D3DINFO_API = `api/setting/visual/update`
|
||||
|
||||
|
@ -11,6 +11,8 @@ import {
|
||||
GET_DASHBOARD_ENERGY_INFO_API,
|
||||
GET_DASHBOARD_ENERGY_COST_API,
|
||||
GET_DASHBOARD_ALARMOPERATION_INFO_API,
|
||||
GET_DASHBOARD_2D3DINFO_API,
|
||||
POST_DASHBOARD_2D3DINFO_API
|
||||
} from "./api";
|
||||
import instance from "@/util/request";
|
||||
import apihandler from "@/util/apiHandler";
|
||||
@ -176,3 +178,22 @@ export const getAlarmOperationInfo = async (building_guid) => {
|
||||
code: res.code,
|
||||
});
|
||||
};
|
||||
|
||||
export const getDashboard2D3D = async (BuildingId) => {
|
||||
const res = await instance.post(GET_DASHBOARD_2D3DINFO_API, {
|
||||
BuildingId});
|
||||
|
||||
return apihandler(res.code, res.data, {
|
||||
msg: res.msg,
|
||||
code: res.code,
|
||||
});
|
||||
};
|
||||
|
||||
export const posttDashboard2D3D = async (formData) => {
|
||||
const res = await instance.post(POST_DASHBOARD_2D3DINFO_API, formData);
|
||||
|
||||
return apihandler(res.code, res.data, {
|
||||
msg: res.msg,
|
||||
code: res.code,
|
||||
});
|
||||
};
|
@ -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`;
|
||||
|
6
src/apis/headquarters/api.js
Normal file
6
src/apis/headquarters/api.js
Normal file
@ -0,0 +1,6 @@
|
||||
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`;
|
||||
|
||||
|
44
src/apis/headquarters/index.js
Normal file
44
src/apis/headquarters/index.js
Normal file
@ -0,0 +1,44 @@
|
||||
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,
|
||||
} 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,
|
||||
});
|
||||
}
|
@ -4,6 +4,10 @@ export const GET_HISTORY_SIDEBAR_API = `/api/History/GetDeviceInfo`;
|
||||
export const GET_HISTORY_POINT_API = `/api/History/GetAllDevPoi`;
|
||||
export const GET_HISTORY_DATA_API = `/api/History/GetHistoryData`;
|
||||
export const GET_HISTORY_EXPORT_API = `/api/ExportHistoryExcel`;
|
||||
export const GET_HISTORY_EXPORT_REPORT_API = `/api/History/GetHistoryExcelReport`;
|
||||
export const GET_HISTORY_EXPORT_CURVE_API = `/api/History/GetHistoricalCurveExcelReport`;
|
||||
export const GET_HISTORY_EXPORT_QUICK_API = `/api/History/GetQuickMeteringExcelReport`;
|
||||
export const GET_HISTORY_EXPORT_CLASS_API = `/api/History/GetElectricityClassificationExcelReport`;
|
||||
|
||||
export const GET_HISTORY_FAVORITE_API = `/api/History/GetHistoryFavorite`;
|
||||
export const POST_HISTORY_FAVORITE_API = `/api/History/SaveHistoryFavorite`;
|
||||
|
@ -7,6 +7,10 @@ import {
|
||||
DELETE_HISTORY_FAVORITE_API,
|
||||
UPDATE_HISTORY_FAVORITE_API,
|
||||
GET_HISTORY_EXPORT_API,
|
||||
GET_HISTORY_EXPORT_REPORT_API,
|
||||
GET_HISTORY_EXPORT_CURVE_API,
|
||||
GET_HISTORY_EXPORT_QUICK_API,
|
||||
GET_HISTORY_EXPORT_CLASS_API,
|
||||
} from "./api";
|
||||
import instance, { fileInstance } from "@/util/request";
|
||||
import apihandler from "@/util/apiHandler";
|
||||
@ -81,7 +85,52 @@ export const getHistoryData = async ({
|
||||
};
|
||||
|
||||
export const getHistoryExportData = async ({
|
||||
Start_date,
|
||||
End_date,
|
||||
Start_time,
|
||||
End_time,
|
||||
Device_list,
|
||||
Points,
|
||||
Type,
|
||||
table_type,
|
||||
}) => {
|
||||
const api =
|
||||
parseInt(table_type) === 1
|
||||
? GET_HISTORY_EXPORT_CURVE_API
|
||||
: parseInt(table_type) === 2
|
||||
? GET_HISTORY_EXPORT_QUICK_API
|
||||
: parseInt(table_type) === 3
|
||||
? GET_HISTORY_EXPORT_CLASS_API
|
||||
: GET_HISTORY_EXPORT_API;
|
||||
|
||||
const res = await fileInstance.post(
|
||||
api,
|
||||
{
|
||||
Start_date: Start_date,
|
||||
End_date: End_date,
|
||||
Start_time: Start_time,
|
||||
End_time: End_time,
|
||||
Points: Array.isArray(Points) ? Points : [Points],
|
||||
Device_list: Array.isArray(Device_list) ? Device_list : [Device_list],
|
||||
Type: parseInt(Type),
|
||||
Building_tag_list: [...new Set(Device_list.map((d) => d.split("_")[1]))],
|
||||
table_type: parseInt(table_type),
|
||||
},
|
||||
{ responseType: "blob" }
|
||||
);
|
||||
|
||||
return apihandler(
|
||||
res.code,
|
||||
res,
|
||||
{
|
||||
msg: res.msg,
|
||||
code: res.code,
|
||||
},
|
||||
downloadExcel
|
||||
);
|
||||
};
|
||||
|
||||
export const getHistoryExportReport = async ({
|
||||
Start_date,
|
||||
End_date,
|
||||
Start_time,
|
||||
@ -89,19 +138,8 @@ export const getHistoryExportData = async ({
|
||||
Device_list,
|
||||
Points,
|
||||
}) => {
|
||||
/*
|
||||
{
|
||||
Type,
|
||||
Start_date,
|
||||
End_date,
|
||||
Start_time,
|
||||
End_time,
|
||||
Device_list,
|
||||
Points,
|
||||
}
|
||||
*/
|
||||
const res = await fileInstance.post(
|
||||
GET_HISTORY_EXPORT_API,
|
||||
GET_HISTORY_EXPORT_REPORT_API,
|
||||
{
|
||||
// ...exportContent,
|
||||
Start_date: Start_date,
|
||||
@ -110,7 +148,6 @@ export const getHistoryExportData = async ({
|
||||
End_time: End_time,
|
||||
Points: Array.isArray(Points) ? Points : [Points],
|
||||
Device_list: Array.isArray(Device_list) ? Device_list : [Device_list],
|
||||
Type: parseInt(Type),
|
||||
Building_tag_list: [...new Set(Device_list.map((d) => d.split("_")[1]))],
|
||||
},
|
||||
{ responseType: "blob" }
|
||||
|
@ -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_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,
|
||||
});
|
||||
}
|
||||
|
@ -58,6 +58,7 @@
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
font-weight: normal;
|
||||
scrollbar-color: auto !important;
|
||||
}
|
||||
|
||||
body {
|
||||
|
@ -36,7 +36,7 @@ const toggleErrIcon = () => {
|
||||
</div>
|
||||
<div
|
||||
v-if="showErr"
|
||||
class="drawer-side translate-y-20 max-h-[90vh] overflow-x-hidden overflow-y-scroll"
|
||||
class="drawer-side translate-y-20 max-h-[90vh] overflow-x-hidden overflow-y-scroll w-[300px] left-auto right-0"
|
||||
>
|
||||
<AlarmCards />
|
||||
</div>
|
||||
|
@ -1,6 +1,6 @@
|
||||
<script setup>
|
||||
import * as echarts from "echarts";
|
||||
import { onMounted, ref, markRaw } from "vue";
|
||||
import { onMounted, ref, markRaw, nextTick } from "vue";
|
||||
import axios from "axios";
|
||||
|
||||
const props = defineProps({
|
||||
@ -24,9 +24,15 @@ async function updateSvg(svg, option) {
|
||||
} else {
|
||||
clear();
|
||||
}
|
||||
axios.get(svg.path).then(({ data }) => {
|
||||
axios.get(svg.path).then(async ({ data }) => {
|
||||
echarts.registerMap(svg.full_name, { svg: data });
|
||||
chart.value.setOption(option);
|
||||
await nextTick();
|
||||
// 延遲執行以避免在主進程中調用 setOption
|
||||
setTimeout(() => {
|
||||
if (chart.value && !chart.value.isDisposed()) {
|
||||
chart.value.setOption(option);
|
||||
}
|
||||
}, 0);
|
||||
if (props.getCoordinate) {
|
||||
chart.value.getZr().on("click", function (params) {
|
||||
var pixelPoint = [params.offsetX, params.offsetY];
|
||||
@ -44,11 +50,15 @@ async function updateSvg(svg, option) {
|
||||
value: dataPoint, // 當前座標值
|
||||
itemStyle: { color: "#0000FF" }, // 設為藍色
|
||||
});
|
||||
chart.value.setOption({
|
||||
series: {
|
||||
data: updatedData,
|
||||
},
|
||||
});
|
||||
setTimeout(() => {
|
||||
if (chart.value && !chart.value.isDisposed()) {
|
||||
chart.value.setOption({
|
||||
series: {
|
||||
data: updatedData,
|
||||
},
|
||||
});
|
||||
}
|
||||
}, 0);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
@ -64,10 +64,11 @@ watch(
|
||||
twMerge(
|
||||
'flex-col text-xl',
|
||||
cls,
|
||||
openChildren.includes(dataParentKey) || open ? 'flex' : 'hidden'
|
||||
openChildren.includes(d.key) || open ? 'flex' : 'hidden'
|
||||
)
|
||||
"
|
||||
v-for="d in data"
|
||||
:key="d.key"
|
||||
:data-parent="d.key"
|
||||
:open="open"
|
||||
>
|
||||
|
@ -9,7 +9,7 @@ const props = defineProps({
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
value: String,
|
||||
value: Object,
|
||||
isTopLabelExist: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
|
@ -9,7 +9,7 @@ const props = defineProps({
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
value: String,
|
||||
value: Object,
|
||||
isTopLabelExist: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
|
@ -51,7 +51,7 @@ onMounted(() => {
|
||||
: 'focus-visible:outline-none backdrop:bg-transparent',
|
||||
)" :style="modalStyle" v-draggable="draggable">
|
||||
<div :class="twMerge(
|
||||
'modal-box static rounded-md border border-info py-5 px-6 overflow-y-scroll bg-normal',
|
||||
'modal-box static rounded-md border border-info py-5 px-6 overflow-y-auto bg-normal',
|
||||
modalClass
|
||||
)
|
||||
" :style="{ minWidth: isNaN(width) ? width : `${width}px` }">
|
||||
|
@ -4,7 +4,7 @@ import { twMerge } from "tailwind-merge";
|
||||
|
||||
const props = defineProps({
|
||||
name: String,
|
||||
value: String,
|
||||
value: Object,
|
||||
items: Array,
|
||||
isLabelExist: {
|
||||
type: Boolean,
|
||||
|
@ -18,7 +18,7 @@ const props = defineProps({
|
||||
Attribute: String,
|
||||
onChange: Function,
|
||||
selectClass: String,
|
||||
value: String || Number,
|
||||
value: Object,
|
||||
isTopLabelExist: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
|
@ -18,7 +18,7 @@ const props = defineProps({
|
||||
Attribute: String,
|
||||
onChange: Function,
|
||||
selectClass: String,
|
||||
value: String || Number,
|
||||
value: Object,
|
||||
isTopLabelExist: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
@ -70,9 +70,7 @@ const props = defineProps({
|
||||
:class="twMerge(disabled ? `text-white` : 'text-dark')"
|
||||
:value="option.value || option.key || option"
|
||||
>
|
||||
<span>
|
||||
{{ option[Attribute] || option }}
|
||||
</span>
|
||||
</option>
|
||||
</select>
|
||||
<div :class="twMerge(isBottomLabelExist ? 'label' : '')">
|
||||
|
@ -3,7 +3,7 @@ import { defineProps } from "vue";
|
||||
|
||||
const props = defineProps({
|
||||
name: String,
|
||||
value: String,
|
||||
value: Object,
|
||||
placeholder: String,
|
||||
});
|
||||
</script>
|
||||
@ -15,7 +15,7 @@ const props = defineProps({
|
||||
<span class="label-text-alt"> <slot name="topRight"></slot></span>
|
||||
</div>
|
||||
<textarea
|
||||
class="textarea text-lg rounded-md border-info focus-within:border-info h-24"
|
||||
class="textarea text-lg rounded-md border-info focus-within:border-info h-40"
|
||||
:placeholder="placeholder"
|
||||
:name="name"
|
||||
v-model="value[name]"
|
||||
|
@ -1,17 +1,22 @@
|
||||
<script setup>
|
||||
import { onMounted, ref } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import NavbarItem from "./NavbarItem.vue";
|
||||
import NavbarBuilding from "./NavbarBuilding.vue";
|
||||
import Logo from "@/assets/img/logo.svg";
|
||||
import useUserInfoStore from "@/stores/useUserInfoStore";
|
||||
import useBuildingStore from "@/stores/useBuildingStore";
|
||||
|
||||
import AlarmDrawer from "@/components/alarm/AlarmDrawer.vue";
|
||||
import NavbarLang from "./NavbarLang.vue";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
const user = ref("");
|
||||
const menuShow = ref(true);
|
||||
const router = useRouter();
|
||||
|
||||
const store = useUserInfoStore();
|
||||
const storeBuilding = useBuildingStore();
|
||||
onMounted(() => {
|
||||
const name = store.user.user_name;
|
||||
if (name) {
|
||||
@ -24,12 +29,26 @@ const toggleMenu = () => {
|
||||
};
|
||||
|
||||
const src = import.meta.env.MODE === "production" ? "./logo.svg" : Logo;
|
||||
|
||||
const logout = () => {
|
||||
document.cookie = "JWT-Authorization=; Max-Age=0";
|
||||
document.cookie = "user_name=; Max-Age=0";
|
||||
store.user.token = "";
|
||||
store.user.user_name = "";
|
||||
storeBuilding.deleteBuilding();
|
||||
router.push({ path: "/login" });
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<header class="navbar bg-dark text-light-info w-full relative z-50">
|
||||
<div class="navbar-start min-w-[480px] lg:min-w-[440px]">
|
||||
<div tabindex="0" role="button" class="btn btn-ghost lg:hidden" @click="toggleMenu">
|
||||
<div
|
||||
tabindex="0"
|
||||
role="button"
|
||||
class="btn btn-ghost lg:hidden"
|
||||
@click="toggleMenu"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-5 w-5"
|
||||
@ -93,12 +112,12 @@ const src = import.meta.env.MODE === "production" ? "./logo.svg" : Logo;
|
||||
class="dropdown-content translate-y-2 z-[100] menu py-3 shadow rounded w-32 bg-[#4c625e] border text-center"
|
||||
>
|
||||
<li class="text-white">
|
||||
<router-link
|
||||
to="logout"
|
||||
type="link"
|
||||
<a
|
||||
href="#"
|
||||
@click.prevent="logout"
|
||||
class="flex flex-col justify-center items-center"
|
||||
>{{ $t("sign_out") }}
|
||||
</router-link>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
@ -1,12 +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 會自動更新資料
|
||||
localStorage.setItem("CviBuilding", JSON.stringify(bui));
|
||||
|
||||
if (bui.is_headquarter == true) {
|
||||
router.replace({ path: "/headquarters" });
|
||||
} else {
|
||||
router.replace({ path: "/dashboard" });
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
@ -15,29 +21,35 @@ onMounted(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="dropdown dropdown-bottom">
|
||||
<div
|
||||
tabindex="0"
|
||||
role="button"
|
||||
class="text-white ml-8 text-lg font-semiLight"
|
||||
>
|
||||
{{ store.selectedBuilding?.full_name }}
|
||||
<font-awesome-icon :icon="['fas', 'angle-down']" class="ml-1" />
|
||||
</div>
|
||||
<ul
|
||||
tabindex="0"
|
||||
class="dropdown-content w-48 left-8 translate-y-2 z-[1] menu py-3 shadow rounded bg-[#4c625e] border text-center"
|
||||
>
|
||||
<li
|
||||
class="text-white my-1 text-base cursor-pointer"
|
||||
v-for="bui in store.buildings"
|
||||
:key="bui.building_tag"
|
||||
@click="selectBuilding(bui)"
|
||||
<template v-if="store.buildings.length > 1">
|
||||
<div class="dropdown dropdown-bottom">
|
||||
<div
|
||||
tabindex="0"
|
||||
role="button"
|
||||
class="text-white ml-8 text-lg font-semiLight"
|
||||
>
|
||||
{{ bui.full_name }}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
{{ store.selectedBuilding?.full_name }}
|
||||
<font-awesome-icon :icon="['fas', 'angle-down']" class="ml-1" />
|
||||
</div>
|
||||
<ul
|
||||
tabindex="0"
|
||||
class="dropdown-content w-48 left-8 translate-y-2 z-[1] menu py-3 shadow rounded bg-[#4c625e] border text-center"
|
||||
>
|
||||
<li
|
||||
class="text-white my-1 text-base cursor-pointer"
|
||||
v-for="bui in store.buildings"
|
||||
:key="bui.building_tag"
|
||||
@click="selectBuilding(bui)"
|
||||
>
|
||||
{{ bui.full_name }}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="text-white ml-8 text-lg font-semiLight">
|
||||
{{ store.selectedBuilding?.full_name }}
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
<style lang="scss" scoped></style>
|
||||
|
@ -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
|
||||
@ -192,13 +190,13 @@ onMounted(() => {
|
||||
params: {
|
||||
main_system_id: main.main_system_tag,
|
||||
sub_system_id: sub.sub_system_tag,
|
||||
...(currentAuthCode === 'PF2' || currentAuthCode === 'PF11'
|
||||
...(currentAuthCode === 'PF2' || currentAuthCode === 'PF11'
|
||||
? { type: sub.type }
|
||||
: { floor_id: 'main' }),
|
||||
},
|
||||
}"
|
||||
>
|
||||
{{ sub.full_name }}
|
||||
{{ sub.resource ? $t(`navbar.${sub.resource}`): sub.full_name }}
|
||||
</router-link>
|
||||
</a-menu-item>
|
||||
</a-sub-menu>
|
||||
|
@ -13,6 +13,38 @@
|
||||
"name": "名称",
|
||||
"time": "时间"
|
||||
},
|
||||
"navbar": {
|
||||
"home": "首页",
|
||||
"sysMonBtnList": "系统监控",
|
||||
"historyData": "历史资料",
|
||||
"energyManagement": "能源管理",
|
||||
"alert": "告警",
|
||||
"operation": "运维管理",
|
||||
"graphManagement": "图资管理",
|
||||
"AssetManagement": "资产管理",
|
||||
"accountManagement": "帐号管理",
|
||||
"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 +121,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 +187,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": "显示警告",
|
||||
@ -399,9 +433,14 @@
|
||||
"sure_to_delete_permanent": "是否确认永久删除该项目?",
|
||||
"delete_success": "删除成功",
|
||||
"delete_failed": "删除失败",
|
||||
"mqtt_refresh": "重新设定成功"
|
||||
"mqtt_refresh": "重新设定成功",
|
||||
"schema_name_required": "架构名称栏位必填",
|
||||
"incorrect_format":"格式不正确",
|
||||
"send_successfully":"送出成功",
|
||||
"edit_successfully":"修改成功"
|
||||
},
|
||||
"setting": {
|
||||
"electricity_meter": "电表",
|
||||
"MQTT_parse": "MQTT 解析",
|
||||
"schema": "架构",
|
||||
"point": "点位",
|
||||
@ -411,6 +450,9 @@
|
||||
"number_of_decimal_places": "小数位数",
|
||||
"boolean_value": "布林值",
|
||||
"hide_point": "点位显示",
|
||||
"hide_switch": "switch 功能",
|
||||
"switch_on_message": "switch 开启时传送的讯息",
|
||||
"switch_off_message": "switch 关闭时传送的讯息",
|
||||
"schema_name": "架构名称",
|
||||
"IoT_point_structure": "IoT点位结构",
|
||||
"system_point_name": "系统点位名称",
|
||||
|
@ -10,9 +10,41 @@
|
||||
"in_otal": "筆資料",
|
||||
"skip_to": "跳至",
|
||||
"serial_number": "序號",
|
||||
"name": "姓名",
|
||||
"name": "名稱",
|
||||
"time": "時間"
|
||||
},
|
||||
"navbar": {
|
||||
"home": "首頁",
|
||||
"sysMonBtnList": "系統監控",
|
||||
"historyData": "歷史資料",
|
||||
"energyManagement": "能源管理",
|
||||
"alert": "告警",
|
||||
"operation": "運維管理",
|
||||
"graphManagement": "圖資管理",
|
||||
"AssetManagement": "資產管理",
|
||||
"accountManagement": "帳號管理",
|
||||
"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 +121,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 +187,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": "顯示警告",
|
||||
@ -399,9 +433,14 @@
|
||||
"sure_to_delete_permanent": "是否確認永久刪除該項目?",
|
||||
"delete_success": "刪除成功",
|
||||
"delete_failed": "刪除失敗",
|
||||
"mqtt_refresh": "重新設定成功"
|
||||
"mqtt_refresh": "重新設定成功",
|
||||
"schema_name_required": "架構名稱欄位必填",
|
||||
"incorrect_format": "格式不正確",
|
||||
"send_successfully": "送出成功",
|
||||
"edit_successfully": "修改成功"
|
||||
},
|
||||
"setting": {
|
||||
"electricity_meter": "電表",
|
||||
"MQTT_parse": "MQTT 解析",
|
||||
"schema": "架構",
|
||||
"point": "點位",
|
||||
@ -411,6 +450,9 @@
|
||||
"number_of_decimal_places": "小數位數",
|
||||
"boolean_value": "布林值",
|
||||
"hide_point": "點位顯示",
|
||||
"hide_switch": "switch 功能",
|
||||
"switch_on_message": "switch 開啟時傳送的訊息",
|
||||
"switch_off_message": "switch 關閉時傳送的訊息",
|
||||
"schema_name": "架構名稱",
|
||||
"IoT_point_structure": "IoT點位結構",
|
||||
"system_point_name": "系統點位名稱",
|
||||
|
@ -13,6 +13,38 @@
|
||||
"name": "Name",
|
||||
"time": "Time"
|
||||
},
|
||||
"navbar": {
|
||||
"home": "Home",
|
||||
"sysMonBtnList": "Monitoring",
|
||||
"historyData": "History Data",
|
||||
"energyManagement": "Energy",
|
||||
"alert": "Alert",
|
||||
"operation": "Maintenance",
|
||||
"graphManagement": "Graph",
|
||||
"AssetManagement": "Devices",
|
||||
"accountManagement": "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 +121,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 +187,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",
|
||||
@ -399,9 +433,14 @@
|
||||
"sure_to_delete_permanent": "Are you sure you want to permanently delete this item?",
|
||||
"delete_success": "Delete successfully",
|
||||
"delete_failed": "Delete failed",
|
||||
"mqtt_refresh": "MQTT reset successful"
|
||||
"mqtt_refresh": "MQTT reset successful",
|
||||
"schema_name_required": "The schema name field is required",
|
||||
"incorrect_format":"Incorrect format",
|
||||
"send_successfully":"Sent successfully",
|
||||
"edit_successfully":"Edited successfully"
|
||||
},
|
||||
"setting": {
|
||||
"electricity_meter": "Electricity Meter",
|
||||
"MQTT_parse": "MQTT Parse",
|
||||
"schema": "Schema",
|
||||
"point": "Point",
|
||||
@ -411,6 +450,9 @@
|
||||
"number_of_decimal_places": "Number of Decimal Places",
|
||||
"boolean_value": "Boolean Value",
|
||||
"hide_point": "Point Display",
|
||||
"hide_switch": "Switch Function",
|
||||
"switch_on_message": "Switch On Message",
|
||||
"switch_off_message": "Switch Off Message",
|
||||
"schema_name": "Schema name",
|
||||
"IoT_point_structure": "IoT Point Structure",
|
||||
"system_point_name": "System Point Name",
|
||||
|
@ -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",
|
||||
},
|
||||
];
|
||||
|
@ -65,7 +65,7 @@ import {
|
||||
faClock,
|
||||
faCheckCircle
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { faCircle } from "@fortawesome/free-regular-svg-icons";
|
||||
import { faCircle,faPaperPlane } from "@fortawesome/free-regular-svg-icons";
|
||||
|
||||
/* add icons to the library */
|
||||
library.add(
|
||||
@ -130,7 +130,8 @@ library.add(
|
||||
faCrown,
|
||||
faClock,
|
||||
faCheckCircle,
|
||||
faCircle
|
||||
faCircle,
|
||||
faPaperPlane
|
||||
);
|
||||
|
||||
export default library;
|
||||
|
@ -6,7 +6,7 @@ import useForgeHeatmap from "./useForgeHeatmap";
|
||||
import useForgeFloor from "./useForgeFloor";
|
||||
|
||||
export default function useForgeSprite() {
|
||||
const { subscribeData } = inject("system_deviceList");
|
||||
const { subscribeData, realtimeData } = inject("system_deviceList");
|
||||
const { getCurrentInfoModalData, clearSelectedDeviceInfo, selected_dbid } =
|
||||
inject("system_selectedDevice");
|
||||
const forgeViewer = ref(null);
|
||||
@ -25,7 +25,7 @@ export default function useForgeSprite() {
|
||||
forgeViewer.value.navigation.setView(newPosition, newTarget);
|
||||
|
||||
// 確保 Home 視角
|
||||
forgeViewer.value.autocam.setCurrentViewAsHome(true);
|
||||
forgeViewer.value.autocam.setCurrentViewAsHome(true);
|
||||
};
|
||||
|
||||
const updateDataVisualization = async (viewer) => {
|
||||
@ -65,6 +65,23 @@ export default function useForgeSprite() {
|
||||
|
||||
const { flatSubData } = useSystemShowData();
|
||||
|
||||
// 根據設備取得即時狀態顏色
|
||||
const getDeviceRealtimeColor = (d) => {
|
||||
if (d.full_name === "SmartSocket-AA001") return "#ff0000";
|
||||
if (
|
||||
d.full_name === "SmartSocket-AA003" ||
|
||||
d.full_name === "SmartSocket-AA004"
|
||||
)
|
||||
return "#888888";
|
||||
const realtimeDevice = realtimeData?.value?.find(
|
||||
(item) => item.device_number === d.device_number
|
||||
);
|
||||
const state = realtimeDevice?.state || "";
|
||||
if (state === "offnormal" || state === "")
|
||||
return d.device_close_color || "#999999";
|
||||
return d.device_normal_color || "#009100";
|
||||
};
|
||||
|
||||
// 創建 sprites
|
||||
const createSprites = async () => {
|
||||
if (dataVizExtn.value) {
|
||||
@ -74,28 +91,25 @@ export default function useForgeSprite() {
|
||||
let spriteColor = new THREE.Color(0xffffff);
|
||||
const BASEURL = window.env?.VITE_FILE_API_BASEURL;
|
||||
const spriteIconUrl = `${BASEURL}/dist/hotspot.svg`;
|
||||
const style = new DataVizCore.ViewableStyle(
|
||||
viewableType,
|
||||
spriteColor,
|
||||
spriteIconUrl
|
||||
);
|
||||
const viewableData = new DataVizCore.ViewableData();
|
||||
viewableData.spriteSize = 24; // Sprites as points of size 24 x 24 pixels
|
||||
flatSubData.value?.forEach((d, index) => {
|
||||
if (d.device_coordinate_3d) {
|
||||
const position = d.device_coordinate_3d;
|
||||
style.color = new THREE.Color(hexToRgb(d.device_normal_color));
|
||||
// 每個都 new 一個 style
|
||||
const pointStyle = new DataVizCore.ViewableStyle(
|
||||
viewableType,
|
||||
new THREE.Color(hexToRgb(getDeviceRealtimeColor(d))),
|
||||
spriteIconUrl
|
||||
);
|
||||
const viewable = new DataVizCore.SpriteViewable(
|
||||
position,
|
||||
style,
|
||||
pointStyle,
|
||||
d.spriteDbId
|
||||
);
|
||||
viewableData.addViewable(viewable);
|
||||
}
|
||||
});
|
||||
// await viewableData.finish();
|
||||
// dataVizExtn.value.addViewables(viewableData);
|
||||
// console.log(dataVizExtn.value);
|
||||
viewableData.finish().then(
|
||||
() => {
|
||||
dataVizExtn.value.addViewables(viewableData);
|
||||
@ -107,6 +121,16 @@ export default function useForgeSprite() {
|
||||
);
|
||||
}
|
||||
};
|
||||
// 監聽 realtimeData 變化,重建 sprites
|
||||
watch(
|
||||
() => realtimeData?.value,
|
||||
() => {
|
||||
if (forgeViewer.value?.isLoadDone()) {
|
||||
createSprites();
|
||||
}
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
|
||||
watch(
|
||||
() => flatSubData,
|
||||
|
@ -9,12 +9,14 @@ 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 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";
|
||||
|
||||
|
||||
import Test from "@/views/Test.vue";
|
||||
import SystemMain from "@/views/system/SystemMain.vue";
|
||||
|
||||
@ -90,6 +92,11 @@ const router = createRouter({
|
||||
name: "setting",
|
||||
component: SettingManagement,
|
||||
},
|
||||
{
|
||||
path: "/headquarters",
|
||||
name: "headquarters",
|
||||
component: HeadquartersManagement,
|
||||
},
|
||||
{
|
||||
path: "/mytestfile/mjm",
|
||||
name: "mytestfile",
|
||||
@ -106,18 +113,11 @@ router.beforeEach(async (to, from, next) => {
|
||||
const auth = useUserInfoStore();
|
||||
const token = useGetCookie("JWT-Authorization");
|
||||
const user_name = useGetCookie("user_name");
|
||||
|
||||
if (to.path === "/logout") {
|
||||
document.cookie = "JWT-Authorization=; Max-Age=0";
|
||||
document.cookie = "user_name=; Max-Age=0";
|
||||
auth.user.token = "";
|
||||
auth.user.user_name = "";
|
||||
window.location.reload();
|
||||
next({ path: "/login" });
|
||||
}
|
||||
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";
|
||||
|
@ -2,6 +2,7 @@ import { defineStore } from "pinia";
|
||||
import { ref, computed, watch } from "vue";
|
||||
import { useRoute } from "vue-router";
|
||||
import { getBuildings } from "@/apis/building";
|
||||
import { getDashboard2D3D } from "@/apis/dashboard";
|
||||
import { getAssetFloorList, getDepartmentList } from "@/apis/asset";
|
||||
|
||||
const useBuildingStore = defineStore("buildingInfo", () => {
|
||||
@ -11,6 +12,9 @@ const useBuildingStore = defineStore("buildingInfo", () => {
|
||||
const floorList = ref([]);
|
||||
const deptList = ref([]);
|
||||
const mainSubSys = ref([]);
|
||||
// 控制顯示2D/3D切換與內容
|
||||
const showForgeArea = ref(true);
|
||||
const previewImageExt = ref("");
|
||||
|
||||
// 計算屬性
|
||||
const mainSys = computed(() =>
|
||||
@ -45,38 +49,62 @@ const useBuildingStore = defineStore("buildingInfo", () => {
|
||||
|
||||
// 獲取所有建築物
|
||||
const fetchBuildings = async () => {
|
||||
const res = await getBuildings();
|
||||
buildings.value = res.data;
|
||||
if (res.data.length > 0 && !selectedBuilding.value) {
|
||||
const storedBuilding = JSON.parse(localStorage.getItem("CviBuilding"));
|
||||
selectedBuilding.value = storedBuilding || res.data[0]; // 預設選第一個建築
|
||||
// const res = await getBuildings();
|
||||
buildings.value = JSON.parse(localStorage.getItem("CviBuildingList")) || [];
|
||||
const storedBuilding = JSON.parse(localStorage.getItem("CviBuilding"));
|
||||
if (buildings.value.length > 0) {
|
||||
selectedBuilding.value = storedBuilding || buildings.value[0]; // 預設選第一個建築
|
||||
} else {
|
||||
selectedBuilding.value = null; // 如果沒有建築物,清空選擇
|
||||
}
|
||||
};
|
||||
|
||||
// 獲取樓層資料
|
||||
const fetchFloorList = async (building_guid) => {
|
||||
const res = await getAssetFloorList(building_guid);
|
||||
floorList.value = res.data[0]?.floors.map((d) => ({
|
||||
...d,
|
||||
title: d.full_name,
|
||||
key: d.floor_guid,
|
||||
})) || [];
|
||||
floorList.value =
|
||||
res.data[0]?.floors.map((d) => ({
|
||||
...d,
|
||||
title: d.full_name,
|
||||
key: d.floor_guid,
|
||||
})) || [];
|
||||
};
|
||||
|
||||
// 獲取部門資料
|
||||
const fetchDepartmentList = async () => {
|
||||
const res = await getDepartmentList();
|
||||
deptList.value = res.data.map((d) => ({
|
||||
...d,
|
||||
title: d.name,
|
||||
key: d.id,
|
||||
})) || [];
|
||||
deptList.value =
|
||||
res.data.map((d) => ({
|
||||
...d,
|
||||
title: d.name,
|
||||
key: d.id,
|
||||
})) || [];
|
||||
};
|
||||
|
||||
// 獲取2D、3D顯示與否
|
||||
const fetchDashboard2D3D = async (BuildingId) => {
|
||||
const res = await getDashboard2D3D(BuildingId);
|
||||
showForgeArea.value = res.data.is3DEnabled || false;
|
||||
previewImageExt.value = res.data.previewImageExt || "";
|
||||
};
|
||||
|
||||
// 清除localStorage建築物
|
||||
const deleteBuilding = () => {
|
||||
localStorage.removeItem("CviBuildingList");
|
||||
localStorage.removeItem("CviBuilding");
|
||||
buildings.value = [];
|
||||
selectedBuilding.value = null;
|
||||
};
|
||||
|
||||
// 當 selectedBuilding 改變時,更新 floorList 和 deptList
|
||||
watch(selectedBuilding, async (newBuilding) => {
|
||||
if (newBuilding) {
|
||||
await Promise.all([fetchFloorList(newBuilding.building_guid), fetchDepartmentList()]);
|
||||
localStorage.setItem("CviBuilding", JSON.stringify(newBuilding));
|
||||
await Promise.all([
|
||||
fetchFloorList(newBuilding.building_guid),
|
||||
fetchDepartmentList(),
|
||||
fetchDashboard2D3D(newBuilding.building_guid),
|
||||
]);
|
||||
}
|
||||
});
|
||||
|
||||
@ -94,9 +122,13 @@ const useBuildingStore = defineStore("buildingInfo", () => {
|
||||
mainSys,
|
||||
subSys,
|
||||
selectedSystem,
|
||||
showForgeArea,
|
||||
previewImageExt,
|
||||
deleteBuilding,
|
||||
fetchBuildings,
|
||||
fetchFloorList,
|
||||
fetchDepartmentList,
|
||||
fetchDashboard2D3D,
|
||||
initialize,
|
||||
};
|
||||
});
|
||||
|
@ -4,7 +4,6 @@ import { ref } from "vue";
|
||||
const useUserInfoStore = defineStore("userInfo", () => {
|
||||
const user = ref({
|
||||
token: "",
|
||||
expires: 0,
|
||||
user_name:"",
|
||||
});
|
||||
|
||||
|
@ -2,38 +2,59 @@ import useGetCookie from "@/hooks/useGetCookie";
|
||||
import axios from "axios";
|
||||
const BASEURL = window.env?.VITE_API_BASEURL;
|
||||
|
||||
// --- 請求攔截器的共用邏輯 ---
|
||||
const requestInterceptor = (config) => {
|
||||
// 確保 headers 物件存在
|
||||
if (!config.headers) {
|
||||
config.headers = {};
|
||||
}
|
||||
|
||||
// 1. 取得並附加最新的 Token
|
||||
const token = useGetCookie("JWT-Authorization");
|
||||
if (token) {
|
||||
// 正確做法:修改屬性,而不是覆蓋整個物件
|
||||
config.headers.Authorization = `Bearer ${token}`;
|
||||
}
|
||||
|
||||
// 2. 取得並附加選定的建築物 GUID
|
||||
const storedBuilding = localStorage.getItem("CviBuilding");
|
||||
if (storedBuilding) {
|
||||
try {
|
||||
const buildingObject = JSON.parse(storedBuilding);
|
||||
if (buildingObject && buildingObject.building_guid) {
|
||||
// 與後端約定好要用哪個標頭,這裡使用 'X-Building-GUID' 作為範例
|
||||
config.headers["X-Building-GUID"] = buildingObject.building_guid;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("解析 localStorage 中的 CviBuilding 失敗:", e);
|
||||
}
|
||||
}
|
||||
return config;
|
||||
};
|
||||
|
||||
const requestErrorInterceptor = (error) => {
|
||||
return Promise.reject(error);
|
||||
};
|
||||
|
||||
|
||||
// --- 一般 API 實例 ---
|
||||
const instance = axios.create({
|
||||
baseURL: BASEURL,
|
||||
timeout: -1,
|
||||
headers: { Authorization: `Bearer ${useGetCookie("JWT-Authorization")}` },
|
||||
timeout: 10000, // 建議設定超時
|
||||
// 移除靜態 headers
|
||||
});
|
||||
|
||||
// Add a request interceptor
|
||||
instance.interceptors.request.use(
|
||||
function (config) {
|
||||
// Do something before request is sent
|
||||
const token = useGetCookie("JWT-Authorization");
|
||||
config.headers = {
|
||||
Authorization: `Bearer ${token}`,
|
||||
};
|
||||
return config;
|
||||
},
|
||||
function (error) {
|
||||
// Do something with request error
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
// 使用共用的攔截器
|
||||
instance.interceptors.request.use(requestInterceptor, requestErrorInterceptor);
|
||||
|
||||
// Add a response interceptor
|
||||
instance.interceptors.response.use(
|
||||
function (response) {
|
||||
// Any status code that lie within the range of 2xx cause this function to trigger
|
||||
// Do something with response data
|
||||
const { status, data, headers } = response;
|
||||
const { data } = response;
|
||||
|
||||
return {
|
||||
...data,
|
||||
};
|
||||
return { ...data };
|
||||
},
|
||||
function (error) {
|
||||
// Any status codes that falls outside the range of 2xx cause this function to trigger
|
||||
@ -45,27 +66,16 @@ instance.interceptors.response.use(
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
// --- 檔案處理 API 實例 ---
|
||||
export const fileInstance = axios.create({
|
||||
baseURL: BASEURL,
|
||||
timeout: -1,
|
||||
headers: { Authorization: `Bearer ${useGetCookie("JWT-Authorization")}` },
|
||||
// 移除靜態 headers
|
||||
});
|
||||
|
||||
// Add a request interceptor
|
||||
fileInstance.interceptors.request.use(
|
||||
function (config) {
|
||||
// Do something before request is sent
|
||||
const token = useGetCookie("JWT-Authorization");
|
||||
config.headers = {
|
||||
Authorization: `Bearer ${token}`,
|
||||
};
|
||||
return config;
|
||||
},
|
||||
function (error) {
|
||||
// Do something with request error
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
// 使用共用的攔截器
|
||||
fileInstance.interceptors.request.use(requestInterceptor, requestErrorInterceptor);
|
||||
|
||||
// Add a response interceptor
|
||||
fileInstance.interceptors.response.use(
|
||||
|
@ -61,9 +61,8 @@ const onReset = () => {
|
||||
? t('assetManagement.edit_system_category')
|
||||
: t('assetManagement.add_system_category')
|
||||
"
|
||||
:open="open"
|
||||
:onCancel="onReset"
|
||||
width="300"
|
||||
:width="300"
|
||||
>
|
||||
<template #modalContent>
|
||||
<form ref="form" class="mt-5 flex flex-col items-center">
|
||||
@ -75,7 +74,7 @@ const onReset = () => {
|
||||
</span></template
|
||||
>
|
||||
</Input>
|
||||
<Input name="system_value" :value="formState" :readonly="props.formState?.id">
|
||||
<Input name="system_value" :value="formState" :readonly="Boolean(props.formState?.id)">
|
||||
<template #topLeft>{{ $t("assetManagement.system_value") }}</template>
|
||||
<template #bottomLeft
|
||||
><span class="text-error text-base">
|
||||
|
@ -95,9 +95,8 @@ const onOk = async () => {
|
||||
? t('assetManagement.edit_device_category')
|
||||
: t('assetManagement.add_device_category')
|
||||
"
|
||||
:open="open"
|
||||
:onCancel="onCancel"
|
||||
width="300"
|
||||
:width="300"
|
||||
>
|
||||
<template #modalContent>
|
||||
<form ref="form" class="mt-5 flex flex-col items-center">
|
||||
@ -112,7 +111,7 @@ const onOk = async () => {
|
||||
<Input
|
||||
name="system_value"
|
||||
:value="formState"
|
||||
:readonly="props.formState?.id"
|
||||
:readonly="Boolean(props.formState?.id)"
|
||||
>
|
||||
<template #topLeft>{{ $t("assetManagement.system_value") }}</template>
|
||||
<template #bottomLeft
|
||||
@ -128,7 +127,7 @@ const onOk = async () => {
|
||||
name="system_parent_id"
|
||||
:value="formState"
|
||||
selectClass="border-info focus-within:border-info"
|
||||
:disabled="props.formState?.id"
|
||||
:disabled="Boolean(props.formState?.id)"
|
||||
>
|
||||
<template #topLeft>{{
|
||||
$t("assetManagement.system_parent")
|
||||
|
@ -4,7 +4,7 @@ import { onMounted, ref, watch, inject, provide, computed } from "vue";
|
||||
import useSearchParam from "@/hooks/useSearchParam";
|
||||
import AssetTableAddModal from "./AssetTableAddModal.vue";
|
||||
import { getOperationCompanyList } from "@/apis/operation";
|
||||
import { getAssetFloorList } from "@/apis/asset";
|
||||
import { postMQTTRefresh } from "@/apis/alert";
|
||||
import dayjs from "dayjs";
|
||||
import { useI18n } from "vue-i18n";
|
||||
const { t } = useI18n();
|
||||
@ -33,7 +33,8 @@ 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,
|
||||
department: departmentList.value.find(({ id }) => d.department_id === id)
|
||||
?.name,
|
||||
buying_date: d?.buying_date
|
||||
? dayjs(d?.buying_date).format("YYYY-MM-DD")
|
||||
: "",
|
||||
@ -44,6 +45,15 @@ const getAssetData = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
const refreshMQTT = async () => {
|
||||
const res = await postMQTTRefresh();
|
||||
if (res.isSuccess) {
|
||||
openToast("success", t("msg.mqtt_refresh"));
|
||||
} else {
|
||||
openToast("error", res.msg, "#outliers_add_table_item");
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
getAssetData();
|
||||
});
|
||||
@ -113,8 +123,8 @@ watch(
|
||||
(newValue) => {
|
||||
if (newValue.value?.subSys_id) {
|
||||
getAssetData();
|
||||
}else{
|
||||
tableData.value=[];
|
||||
} else {
|
||||
tableData.value = [];
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -162,7 +172,7 @@ const remove = async (id) => {
|
||||
const res = await deleteAssetItem(id);
|
||||
if (res.isSuccess) {
|
||||
getAssetData();
|
||||
openToast("success", t("msg.delete_success"));
|
||||
openToast("success", t("msg.delete_success"));
|
||||
} else {
|
||||
openToast("error", res.msg);
|
||||
}
|
||||
@ -175,14 +185,21 @@ provide("asset_table_data", {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex justify-start items-center mt-10">
|
||||
<h3 class="text-xl mr-5">{{ $t("assetManagement.device_list") }}</h3>
|
||||
<AssetTableAddModal
|
||||
:openModal="openModal"
|
||||
:onCancel="onCancel"
|
||||
:editRecord="editRecord"
|
||||
:getData="getAssetData"
|
||||
/>
|
||||
<div class="flex justify-between items-center mt-10">
|
||||
<div class="flex">
|
||||
<h3 class="text-xl mr-5">{{ $t("assetManagement.device_list") }}</h3>
|
||||
<AssetTableAddModal
|
||||
:openModal="openModal"
|
||||
:onCancel="onCancel"
|
||||
:editRecord="editRecord"
|
||||
:getData="getAssetData"
|
||||
/>
|
||||
</div>
|
||||
<button class="btn btn-sm btn-add" @click.prevent="refreshMQTT">
|
||||
<font-awesome-icon :icon="['fas', 'cog']" />{{
|
||||
$t("alert.reorganization")
|
||||
}}
|
||||
</button>
|
||||
</div>
|
||||
<Table :columns="columns" :dataSource="tableData" class="mt-3">
|
||||
<template #bodyCell="{ record, column, index }">
|
||||
|
@ -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();
|
||||
closeModal();
|
||||
setTimeout(() => {
|
||||
closeModal();
|
||||
}, 1000);
|
||||
} else {
|
||||
openToast("error", res.msg, "#asset_add_table_item");
|
||||
}
|
||||
@ -102,15 +107,22 @@ 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')"
|
||||
:open="open"
|
||||
:title="
|
||||
editRecord?.main_id
|
||||
? $t('assetManagement.edit_device')
|
||||
: $t('assetManagement.add_device')
|
||||
"
|
||||
:onCancel="closeModal"
|
||||
width="1600"
|
||||
:width="1600"
|
||||
>
|
||||
<template #modalContent>
|
||||
<form ref="form" class="grid grid-cols-5 gap-5">
|
||||
|
@ -3,8 +3,6 @@ import { ref, inject, onBeforeMount, onMounted, watch } from "vue";
|
||||
import * as yup from "yup";
|
||||
import "yup-phone-lite";
|
||||
import useSearchParam from "@/hooks/useSearchParam";
|
||||
import AssetTableModalLeftInfoIoT from "./AssetTableModalLeftInfoIoT.vue";
|
||||
import AssetTableModalLeftInfoGraph from "./AssetTableModalLeftInfoGraph.vue";
|
||||
import AssetTableModalLeftInfoMQTT from "./AssetTableModalLeftInfoMQTT.vue";
|
||||
import useUserInfoStore from "@/stores/useUserInfoStore";
|
||||
import dayjs from "dayjs";
|
||||
@ -209,8 +207,6 @@ watch(
|
||||
</Select>
|
||||
</div>
|
||||
<AssetTableModalLeftInfoMQTT />
|
||||
<AssetTableModalLeftInfoGraph />
|
||||
<AssetTableModalLeftInfoIoT />
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
|
@ -127,7 +127,7 @@ const onCancel = () => {
|
||||
id="asset_add_dept"
|
||||
:title="t('assetManagement.department')"
|
||||
:onCancel="onCancel"
|
||||
width="400"
|
||||
:width="400"
|
||||
>
|
||||
<template #modalContent>
|
||||
<form ref="form">
|
||||
|
@ -138,7 +138,7 @@ onMounted(async () => {
|
||||
id="asset_add_graph_item"
|
||||
:title="t('graphManagement.title')"
|
||||
:onCancel="onCancel"
|
||||
width="500"
|
||||
:width="500"
|
||||
>
|
||||
<template #modalContent>
|
||||
<ul class="menu bg-base-200 rounded-box text-lg w-full mt-3">
|
||||
|
@ -225,7 +225,7 @@ const deleteItem = (value) => {
|
||||
id="asset_add_IoT_item"
|
||||
:title="t('assetManagement.associated_device')"
|
||||
:onCancel="onCancel"
|
||||
width="900"
|
||||
:width="900"
|
||||
>
|
||||
<template #modalContent>
|
||||
<ButtonGroup
|
||||
|
@ -1,6 +1,7 @@
|
||||
<script setup>
|
||||
import { onMounted, ref, inject, watch, computed } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { postMQTTpublish } from "@/apis/asset";
|
||||
import mqtt from "mqtt";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
@ -93,12 +94,34 @@ const onCancel = () => {
|
||||
}
|
||||
countdown.value = 60;
|
||||
};
|
||||
|
||||
const onSubmit = async () => {
|
||||
const Topic = formState.value.topic_publish;
|
||||
let Payload = "";
|
||||
try {
|
||||
Payload = JSON.stringify(JSON.parse(formState.value.publish_message));
|
||||
} catch (e) {
|
||||
openToast("error", t("msg.incorrect_format"), "#asset_add_table_item");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await postMQTTpublish({ Topic, Payload });
|
||||
if (res.isSuccess) {
|
||||
openToast("success", t("msg.send_successfully"), "#asset_add_table_item");
|
||||
} else {
|
||||
openToast("error", res.msg, "#asset_add_table_item");
|
||||
}
|
||||
} catch (error) {
|
||||
openToast("error", t("setting.mqtt_send_error"), "#asset_add_table_item");
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex w-72">
|
||||
<div class="flex col-span-2 pb-5">
|
||||
<Input :value="formState" name="topic">
|
||||
<template #topLeft>MQTT Topic</template>
|
||||
<template #topLeft>MQTT subscribe topic</template>
|
||||
</Input>
|
||||
<button type="button" class="btn btn-add mt-11 ms-1" @click="openModal">
|
||||
<font-awesome-icon :icon="['fas', 'cog']" />
|
||||
@ -106,7 +129,20 @@ const onCancel = () => {
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<Modal id="mqtt_test" title="MQTT Topic" :onCancel="onCancel" width="400">
|
||||
<div class="flex flex-col col-span-2 border-t-gray-400 border-t py-5">
|
||||
<Input :value="formState" name="topic_publish">
|
||||
<template #topLeft>MQTT publish topic</template>
|
||||
</Input>
|
||||
<Textarea :value="formState" name="publish_message">
|
||||
<template #topLeft>MQTT messages</template>
|
||||
</Textarea>
|
||||
<button type="button" class="btn btn-add mt-6 w-24" @click="onSubmit">
|
||||
<font-awesome-icon :icon="['far', 'paper-plane']" />
|
||||
Send
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<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">
|
||||
|
@ -2,6 +2,8 @@
|
||||
import { onMounted, ref, inject, onBeforeMount, watch, computed } from "vue";
|
||||
import EffectScatter from "@/components/chart/EffectScatter.vue";
|
||||
import useBuildingStore from "@/stores/useBuildingStore";
|
||||
import AssetTableModalLeftInfoIoT from "./AssetTableModalLeftInfoIoT.vue";
|
||||
import AssetTableModalLeftInfoGraph from "./AssetTableModalLeftInfoGraph.vue";
|
||||
import * as yup from "yup";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { useI18n } from "vue-i18n";
|
||||
@ -115,7 +117,7 @@ const getCoordinate = (position) => {
|
||||
|
||||
<template>
|
||||
<!-- 平面圖 -->
|
||||
|
||||
|
||||
<div class="flex gap-4 mb-5">
|
||||
<Select
|
||||
:value="formState"
|
||||
@ -143,7 +145,7 @@ const getCoordinate = (position) => {
|
||||
></Input
|
||||
>
|
||||
</div>
|
||||
<div class="relative">
|
||||
<div class="relative min-h-[70vh]">
|
||||
<EffectScatter
|
||||
id="asset_floor_chart"
|
||||
ref="asset_floor_chart"
|
||||
@ -157,11 +159,13 @@ const getCoordinate = (position) => {
|
||||
/>
|
||||
<div
|
||||
v-if="!currentFloor?.floor_map_url"
|
||||
class="absolute top-0 left-0 flex justify-center items-center min-h-[500px] w-full border border-stone-900 shadow-lg bg-sub-success bg-opacity-25 rounded-md"
|
||||
class="absolute top-0 left-0 flex justify-center items-center min-h-[70vh] w-full border border-stone-900 shadow-lg bg-sub-success bg-opacity-25 rounded-md"
|
||||
>
|
||||
<p class="text-2xl">{{ $t("assetManagement.add_floor_text") }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<AssetTableModalLeftInfoGraph />
|
||||
<AssetTableModalLeftInfoIoT />
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
|
@ -2,7 +2,7 @@
|
||||
import ButtonGroup from "@/components/customUI/ButtonGroup.vue";
|
||||
import Account from "./components/Account.vue";
|
||||
import Role from "./components/Role.vue";
|
||||
import { computed, watch, onBeforeMount } from "vue";
|
||||
import { computed, watch, onBeforeMount, markRaw } from "vue";
|
||||
import useActiveBtn from "@/hooks/useActiveBtn";
|
||||
import { useI18n } from "vue-i18n";
|
||||
const { t, locale } = useI18n();
|
||||
@ -18,13 +18,13 @@ const initializeItems = () => {
|
||||
title: t("accountManagement.account_title"),
|
||||
key: "account",
|
||||
active: true,
|
||||
component: Account,
|
||||
component: markRaw(Account),
|
||||
},
|
||||
{
|
||||
title: t("accountManagement.role_title"),
|
||||
key: "role",
|
||||
active: false,
|
||||
component: Role,
|
||||
component: markRaw(Role),
|
||||
},
|
||||
]);
|
||||
};
|
||||
|
@ -127,7 +127,7 @@ const onOk = async () => {
|
||||
id="account_user_modal"
|
||||
:title="formState?.Id ? t('button.edit') : t('button.add')"
|
||||
:onCancel="onCancel"
|
||||
width="710"
|
||||
:width="710"
|
||||
>
|
||||
<template #modalContent>
|
||||
<form ref="form" class="mt-5 w-full flex flex-wrap justify-between">
|
||||
|
@ -9,7 +9,7 @@ const { t } = useI18n();
|
||||
const { openToast } = inject("app_toast");
|
||||
|
||||
const props = defineProps({
|
||||
account: String,
|
||||
account: Object,
|
||||
});
|
||||
|
||||
const formState = ref({
|
||||
@ -48,7 +48,7 @@ const onOk = async () => {
|
||||
id="account_user_password_modal"
|
||||
:title="t('accountManagement.change_password')"
|
||||
:onCancel="onCancel"
|
||||
width="710"
|
||||
:width="710"
|
||||
>
|
||||
<template #modalContent>
|
||||
<p class="mt-10 text-3xl">{{ account.Name }}</p>
|
||||
|
@ -13,7 +13,7 @@ import useActiveBtn from "@/hooks/useActiveBtn";
|
||||
import { useI18n } from "vue-i18n";
|
||||
const { t } = useI18n();
|
||||
const props = defineProps({
|
||||
selectedRole: String,
|
||||
selectedRole: Object,
|
||||
cancelModal: Function,
|
||||
disabled: Boolean,
|
||||
update: Function,
|
||||
|
@ -2,7 +2,7 @@
|
||||
import ButtonGroup from "@/components/customUI/ButtonGroup.vue";
|
||||
import AlertQuery from "./components/AlertQuery/AlertQuery.vue";
|
||||
import AlertSetting from "./components/AlertSetting/AlertSetting.vue";
|
||||
import { computed, watch, onBeforeMount } from "vue";
|
||||
import { computed, watch, onBeforeMount, markRaw } from "vue";
|
||||
import useActiveBtn from "@/hooks/useActiveBtn";
|
||||
import { useI18n } from "vue-i18n";
|
||||
const { t, locale } = useI18n();
|
||||
@ -18,13 +18,13 @@ const initializeItems = () => {
|
||||
title: t("alert.query_title"),
|
||||
key: "Query",
|
||||
active: true,
|
||||
component: AlertQuery,
|
||||
component: markRaw(AlertQuery),
|
||||
},
|
||||
{
|
||||
title: t("alert.setting_title"),
|
||||
key: "Setting",
|
||||
active: false,
|
||||
component: AlertSetting,
|
||||
component: markRaw(AlertSetting),
|
||||
},
|
||||
]);
|
||||
};
|
||||
|
@ -160,7 +160,7 @@ watch(
|
||||
id="alert_action_item"
|
||||
:title="t('alert.repair_order')"
|
||||
:onCancel="onCancel"
|
||||
width="710"
|
||||
:width="710"
|
||||
>
|
||||
<template #modalContent>
|
||||
<form ref="form" class="mt-5 w-full flex flex-wrap justify-between">
|
||||
@ -199,7 +199,7 @@ watch(
|
||||
{
|
||||
key: 1,
|
||||
value: 1,
|
||||
title: $t('alert.maintenance'),
|
||||
title: $t('operation.maintenance'),
|
||||
},
|
||||
{
|
||||
key: 2,
|
||||
|
@ -94,9 +94,8 @@ const closeModal = () => {
|
||||
<Modal
|
||||
id="notify_add_table_item"
|
||||
:title="t('alert.notify_list')"
|
||||
:open="open"
|
||||
:onCancel="closeModal"
|
||||
width="300"
|
||||
:width="300"
|
||||
>
|
||||
<template #modalContent>
|
||||
<form ref="form" class="mt-5 flex flex-col items-center">
|
||||
@ -128,7 +127,7 @@ const closeModal = () => {
|
||||
<p class="text-light text-base">{{ $t("alert.notify_items") }}</p>
|
||||
<AlertNoticesTable
|
||||
:SaveCheckAuth="SaveCheckAuth"
|
||||
:NoticeData="[noticeList[2]]"
|
||||
:NoticeData="[noticeList[1]]"
|
||||
:onChange="onChange"
|
||||
/>
|
||||
<span class="text-error text-base">
|
||||
|
@ -119,9 +119,8 @@ const closeModal = () => {
|
||||
<Modal
|
||||
id="outliers_add_table_item"
|
||||
:title="t('alert.alarm_settings')"
|
||||
:open="open"
|
||||
:onCancel="closeModal"
|
||||
width="710"
|
||||
:width="710"
|
||||
>
|
||||
<template #modalContent>
|
||||
<form ref="form" class="mt-5 w-full flex flex-wrap justify-between">
|
||||
|
@ -10,17 +10,17 @@ const { locale } = useI18n();
|
||||
const timesList = ref([]);
|
||||
const noticeList = ref([]);
|
||||
|
||||
const timesListData = async () => {
|
||||
const res = await getAlarmScheduleList();
|
||||
timesList.value = res.data.map((items) => ({
|
||||
...items,
|
||||
key:items.id,
|
||||
schedule_array: JSON.parse(items.schedule_json).map((time, index) => ({
|
||||
day: index + 1,
|
||||
time,
|
||||
})),
|
||||
}));
|
||||
};
|
||||
// const timesListData = async () => {
|
||||
// const res = await getAlarmScheduleList();
|
||||
// timesList.value = res.data.map((items) => ({
|
||||
// ...items,
|
||||
// key:items.id,
|
||||
// schedule_array: JSON.parse(items.schedule_json).map((time, index) => ({
|
||||
// day: index + 1,
|
||||
// time,
|
||||
// })),
|
||||
// }));
|
||||
// };
|
||||
|
||||
const NoticeListData = async () => {
|
||||
const res = await getNoticeList(locale.value);
|
||||
@ -32,11 +32,15 @@ watch(locale, () => {
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
timesListData();
|
||||
// timesListData();
|
||||
NoticeListData();
|
||||
});
|
||||
|
||||
provide("notify_table", { timesList, noticeList, timesListData });
|
||||
provide("notify_table", {
|
||||
timesList,
|
||||
noticeList,
|
||||
// timesListData
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
@ -16,14 +16,14 @@ import { twMerge } from "tailwind-merge";
|
||||
const store = useBuildingStore();
|
||||
const { items, changeActiveBtn, setItems, selectedBtn } = useActiveBtn();
|
||||
let intervalId = null;
|
||||
const energyCostData = ref(null);
|
||||
const energyCostData = ref({});
|
||||
const FILE_BASEURL = import.meta.env.VITE_FILE_API_BASEURL;
|
||||
const imgBaseUrl = ref("");
|
||||
const formState = ref({
|
||||
building_guid: null,
|
||||
floor_guid: "all",
|
||||
department_id: "all",
|
||||
});
|
||||
// 控制顯示2D/3D切換與內容
|
||||
const showForgeArea = ref(true);
|
||||
|
||||
const getEnergyCostData = async (params) => {
|
||||
const res = await getEnergyCost(params);
|
||||
@ -40,6 +40,18 @@ watch(
|
||||
{ immediate: true, deep: true }
|
||||
);
|
||||
|
||||
watch(
|
||||
() => store.previewImageExt,
|
||||
(newExt) => {
|
||||
if (formState.value.building_guid) {
|
||||
imgBaseUrl.value = newExt
|
||||
? `${FILE_BASEURL}/upload/setting/previewImage/${formState.value.building_guid}${newExt}`
|
||||
: "/build_img.jpg";
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
watch(
|
||||
() => formState.value,
|
||||
(newVal) => {
|
||||
@ -68,22 +80,26 @@ watch(
|
||||
{ immediate: true, deep: true }
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
if (showForgeArea.value) {
|
||||
setItems([
|
||||
{
|
||||
title: "2D",
|
||||
key: "2D",
|
||||
active: false,
|
||||
},
|
||||
{
|
||||
title: "3D",
|
||||
key: "3D",
|
||||
active: true,
|
||||
},
|
||||
]);
|
||||
}
|
||||
});
|
||||
watch(
|
||||
() => store.showForgeArea,
|
||||
(newVal) => {
|
||||
if (newVal == true) {
|
||||
setItems([
|
||||
{
|
||||
title: "2D",
|
||||
key: "2D",
|
||||
active: false,
|
||||
},
|
||||
{
|
||||
title: "3D",
|
||||
key: "3D",
|
||||
active: true,
|
||||
},
|
||||
]);
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
onUnmounted(() => {
|
||||
clearInterval(intervalId);
|
||||
@ -94,7 +110,7 @@ onUnmounted(() => {
|
||||
<div class="flex flex-wrap items-center">
|
||||
<!-- 建築圖 -->
|
||||
<div class="w-full xl:w-1/3 relative">
|
||||
<template v-if="showForgeArea">
|
||||
<template v-if="store.showForgeArea">
|
||||
<ButtonConnectedGroup
|
||||
:items="items"
|
||||
className="btn-xs absolute right-3 top-6 z-20 bg-slate-800 p-0 rounded-lg "
|
||||
@ -104,7 +120,7 @@ onUnmounted(() => {
|
||||
<!-- setting頁面要新增讓他能上傳圖片 -->
|
||||
<img
|
||||
alt="build"
|
||||
src="/build_img.jpg"
|
||||
:src="imgBaseUrl"
|
||||
:class="
|
||||
twMerge(
|
||||
'absolute w-full h-full transition-opacity duration-300',
|
||||
@ -114,6 +130,7 @@ onUnmounted(() => {
|
||||
)
|
||||
"
|
||||
/>
|
||||
|
||||
<Forge
|
||||
:class="
|
||||
twMerge(
|
||||
@ -129,7 +146,7 @@ onUnmounted(() => {
|
||||
<template v-else>
|
||||
<img
|
||||
alt="build"
|
||||
src="/build_img.jpg"
|
||||
:src="imgBaseUrl"
|
||||
class="area-img-box w-full h-[460px] block relative rounded-sm mt-3"
|
||||
/>
|
||||
</template>
|
||||
|
@ -26,7 +26,7 @@ const currentEnergyType = ref({
|
||||
|
||||
// 取得當前能耗資料
|
||||
const getCurrentEnergyData = () => {
|
||||
if (!props.energyCostData) {
|
||||
if (!props.energyCostData || !props.energyCostData.rank) {
|
||||
return []; // 或者返回一些默认值
|
||||
}
|
||||
|
||||
|
@ -35,6 +35,16 @@ watch(selectedBtn, (newVal, oldVal) => {
|
||||
detailData.value = props.modalData[newVal.key];
|
||||
}
|
||||
});
|
||||
|
||||
watch(
|
||||
() => props.modalData,
|
||||
(newVal, oldVal) => {
|
||||
if (newVal) {
|
||||
changeActiveBtn(items.value[0]);
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@ -51,13 +61,13 @@ watch(selectedBtn, (newVal, oldVal) => {
|
||||
}
|
||||
"
|
||||
/>
|
||||
<Button
|
||||
<button
|
||||
type="link"
|
||||
class="btn-link btn-text-without-border px-2"
|
||||
@click="onCancel"
|
||||
>
|
||||
<font-awesome-icon :icon="['fas', 'times']" class="text-[#a5abb1]" />
|
||||
</Button>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
<template #modalContent>
|
||||
@ -78,7 +88,7 @@ watch(selectedBtn, (newVal, oldVal) => {
|
||||
<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>
|
||||
|
@ -1,5 +1,5 @@
|
||||
<script setup>
|
||||
import { ref, onMounted, watch } from "vue";
|
||||
import { ref, onMounted, watch, markRaw } from "vue";
|
||||
import { useRoute } from "vue-router";
|
||||
|
||||
import EnergyChart from "./components/EnergyChart/EnergyChart.vue";
|
||||
@ -14,12 +14,12 @@ const updateComponent = () => {
|
||||
|
||||
if (main_system_id === "energy_chart") {
|
||||
if (sub_system_id === "chart") {
|
||||
currentComponent.value = EnergyChart;
|
||||
currentComponent.value = markRaw(EnergyChart);
|
||||
} else {
|
||||
currentComponent.value = EnergyHistoryTable;
|
||||
currentComponent.value = markRaw(EnergyHistoryTable);
|
||||
}
|
||||
} else if (main_system_id === "energy_report") {
|
||||
currentComponent.value = EnergyReport;
|
||||
currentComponent.value = markRaw(EnergyReport);
|
||||
} else {
|
||||
currentComponent.value = null;
|
||||
}
|
||||
|
@ -42,7 +42,7 @@ const onOk = async () => {
|
||||
|
||||
const res = await postEditCarbonValue({
|
||||
...values,
|
||||
"building_guid":store.selectedBuilding.building_guid,
|
||||
building_guid: store.selectedBuilding.building_guid,
|
||||
});
|
||||
if (res.isSuccess) {
|
||||
props.getData();
|
||||
@ -67,19 +67,24 @@ const closeModal = () => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<button class="btn btn-sm btn-success ms-auto me-3" @click.stop.prevent="openModal">
|
||||
<button
|
||||
class="btn btn-sm btn-success ms-auto me-3"
|
||||
@click.stop.prevent="openModal"
|
||||
>
|
||||
{{ $t("button.edit") }}
|
||||
</button>
|
||||
<Modal
|
||||
id="carbon_emission_item"
|
||||
:title="t('energy.edit_carbon_emission')"
|
||||
:onCancel="closeModal"
|
||||
width="300"
|
||||
:width="300"
|
||||
>
|
||||
<template #modalContent>
|
||||
<form ref="form" class="mt-5 flex flex-col items-center">
|
||||
<Input :value="formState" class="w-full" name="coefficient">
|
||||
<template #topLeft>{{$t('energy.carbon_emission_coefficient')}}</template>
|
||||
<template #topLeft>{{
|
||||
$t("energy.carbon_emission_coefficient")
|
||||
}}</template>
|
||||
<template #bottomLeft>
|
||||
<span class="text-error text-base">
|
||||
{{ formErrorMsg.coefficient }}
|
||||
|
@ -1,5 +1,5 @@
|
||||
<script setup>
|
||||
import { ref, onMounted, nextTick, watch, inject } from "vue";
|
||||
import { ref, onMounted, watch, inject } from "vue";
|
||||
import * as echarts from "echarts";
|
||||
import { getRealTimeDist } from "@/apis/energy";
|
||||
import { useI18n } from "vue-i18n";
|
||||
@ -7,6 +7,7 @@ const { search_data } = inject("energy_data");
|
||||
const { t } = useI18n();
|
||||
|
||||
const chartDiv = ref(null);
|
||||
let myChart = null; // 添加 myChart 变量
|
||||
|
||||
const chartOption = {
|
||||
tooltip: {
|
||||
@ -91,12 +92,18 @@ const loadData = async (value) => {
|
||||
chartOption.series[0].links = links;
|
||||
|
||||
// 初始化圖表
|
||||
const myChart = echarts.init(chartDiv.value);
|
||||
if (myChart) {
|
||||
myChart.dispose(); // 銷毀之前的實例
|
||||
}
|
||||
myChart = echarts.init(chartDiv.value);
|
||||
myChart.setOption(chartOption);
|
||||
}
|
||||
} else {
|
||||
// 初始化圖表
|
||||
echarts.init(chartDiv.value).clear();
|
||||
// 清空圖表
|
||||
if (myChart) {
|
||||
myChart.dispose(); // 銷毀之前的實例
|
||||
myChart = null;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@ -115,6 +122,12 @@ watch(
|
||||
deep: true,
|
||||
}
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
// 初始化圖表
|
||||
myChart = echarts.init(chartDiv.value);
|
||||
myChart.setOption(chartOption);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@ -138,4 +151,4 @@ ul li:last-child:after {
|
||||
@apply absolute top-0 bottom-0 left-full block w-full h-[1px] bg-slate-600 m-auto z-10;
|
||||
content: "";
|
||||
}
|
||||
</style>
|
||||
</style>
|
@ -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>
|
||||
|
@ -81,7 +81,7 @@ const onCancel = () => {
|
||||
id="immediate_demand_add_item"
|
||||
:title="t('energy.edit_automatic_demand')"
|
||||
:onCancel="closeModal"
|
||||
width="300"
|
||||
:width="300"
|
||||
>
|
||||
<template #modalContent>
|
||||
<form ref="form" class="mt-5 flex flex-col items-center">
|
||||
|
@ -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>
|
||||
|
@ -40,9 +40,15 @@ const submit = async (e, type = "") => {
|
||||
|
||||
if (type === "export") {
|
||||
const res = await getHistoryExportData({
|
||||
type: searchParams.value.selectedType,
|
||||
...params,
|
||||
...searchParams.value,
|
||||
Type:
|
||||
route.params.type != 1
|
||||
? 2
|
||||
: searchParams.value.Type
|
||||
? searchParams.value.Type
|
||||
: 1,
|
||||
table_type: route.params.type,
|
||||
}).catch((err) => {
|
||||
isToastOpen.value = {
|
||||
open: true,
|
||||
|
@ -132,7 +132,7 @@ const columns = computed(() => {
|
||||
key: "endTime",
|
||||
},
|
||||
{
|
||||
title: t("energy.ectricity_classification"),
|
||||
title: t("energy.electricity_classification"),
|
||||
key: "elecType",
|
||||
},
|
||||
{
|
||||
|
@ -35,9 +35,23 @@ const columns = computed(() => {
|
||||
if (tableData.value && tableData.value.length > 0) {
|
||||
const firstDataItem = tableData.value[0];
|
||||
if (firstDataItem && firstDataItem.data) {
|
||||
firstDataItem.data.forEach((item, index) => {
|
||||
|
||||
const sortedData = [...firstDataItem.data].sort((a, b) =>
|
||||
dayjs(a.time).valueOf() - dayjs(b.time).valueOf()
|
||||
);
|
||||
|
||||
sortedData.forEach((item, index) => {
|
||||
let formatString = "MM/DD"; // 預設格式
|
||||
switch (route.params.type) {
|
||||
case "1":
|
||||
formatString = "MM/DD";
|
||||
break;
|
||||
case "2":
|
||||
// 取得該週的開始與結束日期
|
||||
const startOfWeek = dayjs(item.time).startOf('week');
|
||||
const endOfWeek = dayjs(item.time).endOf('week');
|
||||
formatString = `${startOfWeek.format("MM/DD")}-${endOfWeek.format("MM/DD")}`;
|
||||
break;
|
||||
case "3":
|
||||
formatString = "YYYY/MM";
|
||||
break;
|
||||
@ -45,7 +59,7 @@ const columns = computed(() => {
|
||||
formatString = "YYYY";
|
||||
break;
|
||||
default:
|
||||
formatString = "MM/DD"; // case 1 和 case 2 都是 "MM-DD"
|
||||
formatString = "MM/DD ";
|
||||
break;
|
||||
}
|
||||
const formattedTime = dayjs(item.time).format(formatString);
|
||||
@ -73,8 +87,13 @@ const dataSource = computed(() => {
|
||||
return tableData.value.map((item) => {
|
||||
let subtotalValue = 0;
|
||||
const newData = {}; // 用於儲存 data 的值,方便 Table 組件讀取
|
||||
if (item.data && item.data.length > 0) {
|
||||
item.data.forEach((dataItem, index) => {
|
||||
|
||||
const sortedData = [...item.data].sort((a, b) =>
|
||||
dayjs(a.time).valueOf() - dayjs(b.time).valueOf()
|
||||
);
|
||||
|
||||
if (sortedData && sortedData.length > 0) {
|
||||
sortedData.forEach((dataItem, index) => {
|
||||
const value = parseFloat(dataItem.value || 0);
|
||||
subtotalValue += value;
|
||||
// 將值儲存在 newData 中,key 與 columns 中的 key 對應
|
||||
|
@ -126,7 +126,7 @@ watch(
|
||||
<button class="btn btn-add mr-3" @click.stop.prevent="openModal">
|
||||
<font-awesome-icon :icon="['fas', 'plus']" />{{ $t("button.add") }}
|
||||
</button>
|
||||
<Modal id="graph_add_item" :title="t('graphManagement.upload')" :onCancel="onCancel" width="800">
|
||||
<Modal id="graph_add_item" :title="t('graphManagement.upload')" :onCancel="onCancel" :width="800">
|
||||
<template #modalContent>
|
||||
<form ref="form" class="mt-5">
|
||||
<div class="mb-2">
|
||||
|
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>
|
@ -1,6 +1,6 @@
|
||||
<script setup>
|
||||
import { computed, defineProps, inject, ref, watch } from "vue";
|
||||
import { getHistoryData, getHistoryExportData } from "@/apis/history";
|
||||
import { getHistoryData, getHistoryExportReport } from "@/apis/history";
|
||||
import useSearchParam from "@/hooks/useSearchParam";
|
||||
import { useI18n } from 'vue-i18n';
|
||||
const { t } = useI18n();
|
||||
@ -39,8 +39,7 @@ const submit = async (e, type = "") => {
|
||||
}
|
||||
|
||||
if (type === "export") {
|
||||
const res = await getHistoryExportData({
|
||||
type: searchParams.value.selectedType,
|
||||
const res = await getHistoryExportReport({
|
||||
...params,
|
||||
...searchParams.value,
|
||||
}).catch((err) => {
|
||||
@ -84,15 +83,15 @@ const submitBtns = computed(() => [
|
||||
btn: "btn-search",
|
||||
disabled: isSearchButtonDisabled.value,
|
||||
},
|
||||
// {
|
||||
// title: t("button.export"),
|
||||
// key: "export",
|
||||
// icon: "download",
|
||||
// btn: "btn-export",
|
||||
// active: false,
|
||||
// onClick: (e) => submit(e, "export"),
|
||||
// disabled: isSearchButtonDisabled.value,
|
||||
// },
|
||||
{
|
||||
title: t("button.export"),
|
||||
key: "export",
|
||||
icon: "download",
|
||||
btn: "btn-export",
|
||||
active: false,
|
||||
onClick: (e) => submit(e, "export"),
|
||||
disabled: isSearchButtonDisabled.value,
|
||||
},
|
||||
]);
|
||||
|
||||
const once = ref(false);
|
||||
|
@ -18,7 +18,6 @@ import useBuildingStore from "@/stores/useBuildingStore";
|
||||
import {
|
||||
getHistoryPoints,
|
||||
getHistoryData,
|
||||
getHistoryExportData,
|
||||
} from "@/apis/history";
|
||||
import useSearchParam from "@/hooks/useSearchParam";
|
||||
import dayjs from "dayjs";
|
||||
|
@ -110,7 +110,7 @@ const checkedBuilding = computed(() => {
|
||||
|
||||
if (
|
||||
selectedDeviceNumber.value?.filter(
|
||||
(d) => d.split("_")[1] === building_tag
|
||||
(d) => d?.split("_")[1] === building_tag
|
||||
).length === allDevices.length
|
||||
) {
|
||||
selected.push(building_tag);
|
||||
|
@ -6,10 +6,12 @@ import * as yup from "yup";
|
||||
import useFormErrorMessage from "@/hooks/useFormErrorMessage";
|
||||
import { useRouter } from "vue-router";
|
||||
import useUserInfoStore from "@/stores/useUserInfoStore";
|
||||
import useBuildingStore from "@/stores/useBuildingStore";
|
||||
import { useI18n } from "vue-i18n";
|
||||
const { t } = useI18n();
|
||||
const { openToast } = inject("app_toast");
|
||||
const store = useUserInfoStore();
|
||||
const storeBuild = useBuildingStore();
|
||||
const router = useRouter();
|
||||
|
||||
let schema = yup.object({
|
||||
@ -35,7 +37,15 @@ const doLogin = async () => {
|
||||
const res = await Login(value);
|
||||
if (res.isSuccess) {
|
||||
store.user = res.data;
|
||||
router.replace({ path: "/dashboard" });
|
||||
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);
|
||||
}
|
||||
|
@ -7,7 +7,7 @@ import { twMerge } from "tailwind-merge";
|
||||
import { useI18n } from 'vue-i18n';
|
||||
const { t, locale } = useI18n();
|
||||
const props = defineProps({
|
||||
selected: String,
|
||||
selected: Object,
|
||||
});
|
||||
|
||||
const { searchParams, changeParams } = useSearchParam();
|
||||
|
@ -226,7 +226,7 @@ watch(
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal id="operation_action_item" :onCancel="onCancel" width="710">
|
||||
<Modal id="operation_action_item" :onCancel="onCancel" :width="710">
|
||||
<template #modalContent>
|
||||
<form ref="form" class="mt-5 w-full flex flex-wrap justify-between">
|
||||
<template v-if="searchParams?.work_type < 3">
|
||||
|
@ -1,5 +1,5 @@
|
||||
<script setup>
|
||||
import { ref, onMounted, watch } from "vue";
|
||||
import { ref, onMounted, watch, markRaw } from "vue";
|
||||
import { useRoute } from "vue-router";
|
||||
|
||||
import Dept from "./components/Dept.vue";
|
||||
@ -10,6 +10,7 @@ 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 ViewModeSetting from "./components/ViewModeSetting.vue";
|
||||
// import PointList from "./components/PointList.vue";
|
||||
|
||||
const route = useRoute();
|
||||
@ -19,21 +20,23 @@ const updateComponent = () => {
|
||||
const { main_system_id, sub_system_id } = route.params;
|
||||
|
||||
if (sub_system_id === "Department") {
|
||||
currentComponent.value = Dept;
|
||||
currentComponent.value = markRaw(Dept);
|
||||
} else if (sub_system_id === "ElecType") {
|
||||
currentComponent.value = ElecType;
|
||||
currentComponent.value = markRaw(ElecType);
|
||||
} else if (sub_system_id === "Vendor") {
|
||||
currentComponent.value = Vendor;
|
||||
currentComponent.value = markRaw(Vendor);
|
||||
} else if (sub_system_id === "Floor") {
|
||||
currentComponent.value = Floors;
|
||||
currentComponent.value = markRaw(Floors);
|
||||
} else if (sub_system_id === "Building") {
|
||||
currentComponent.value = Building;
|
||||
currentComponent.value = markRaw(Building);
|
||||
} else if (sub_system_id === "ElecPricing") {
|
||||
currentComponent.value = ElecPriceManagement;
|
||||
currentComponent.value = markRaw(ElecPriceManagement);
|
||||
} else if (sub_system_id === "MQTT_Result") {
|
||||
currentComponent.value = MQTTList;
|
||||
currentComponent.value = markRaw(MQTTList);
|
||||
} else if (sub_system_id === "Demand") {
|
||||
currentComponent.value = Demand;
|
||||
currentComponent.value = markRaw(Demand);
|
||||
} else if (sub_system_id === "ViewModeSetting") {
|
||||
currentComponent.value = markRaw(ViewModeSetting);
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -46,7 +46,7 @@ const onOk = async () => {
|
||||
id="build_modal"
|
||||
:title="props.formState?.building_guid ? t('button.edit') : t('button.add')"
|
||||
:onCancel="onCancel"
|
||||
width="400"
|
||||
:width="400"
|
||||
>
|
||||
<template #modalContent>
|
||||
<form ref="form" class="mt-5 w-full flex flex-wrap justify-between">
|
||||
|
@ -113,7 +113,7 @@ watch(
|
||||
|
||||
<template>
|
||||
<div class="flex justify-start items-center mt-10 mb-5">
|
||||
<h3 class="text-xl mr-5">電表</h3>
|
||||
<h3 class="text-xl mr-5">{{$t("setting.electricity_meter")}}</h3>
|
||||
<button
|
||||
v-if="!isEditing"
|
||||
class="btn btn-sm btn-add mr-3"
|
||||
|
@ -48,7 +48,7 @@ const onOk = async () => {
|
||||
id="dept_modal"
|
||||
:title="props.formState?.id ? t('button.edit') : t('button.add')"
|
||||
:onCancel="onCancel"
|
||||
width="400"
|
||||
:width="400"
|
||||
>
|
||||
<template #modalContent>
|
||||
<form ref="form" class="mt-5 w-full flex flex-wrap justify-between">
|
||||
|
@ -2,7 +2,7 @@
|
||||
import ButtonGroup from "@/components/customUI/ButtonGroup.vue";
|
||||
import ElecPriceRes from "./ElecPriceRes.vue";
|
||||
import ElecPriceStd from "./ElecPriceStd.vue";
|
||||
import { computed, watch, onBeforeMount, ref, provide } from "vue";
|
||||
import { computed, watch, onBeforeMount, ref, provide, markRaw } from "vue";
|
||||
import useActiveBtn from "@/hooks/useActiveBtn";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { getTimeElec } from "@/apis/energy";
|
||||
@ -26,13 +26,13 @@ const initializeItems = () => {
|
||||
title: t("energy.residential"),
|
||||
key: "Residential",
|
||||
active: true,
|
||||
component: ElecPriceRes,
|
||||
component: markRaw(ElecPriceRes),
|
||||
},
|
||||
{
|
||||
title: t("energy.standard"),
|
||||
key: "Standard",
|
||||
active: false,
|
||||
component: ElecPriceStd,
|
||||
component: markRaw(ElecPriceStd),
|
||||
},
|
||||
]);
|
||||
};
|
||||
@ -93,6 +93,11 @@ provide("time_elec", { sim2, sim3, stand2, stand3, getData });
|
||||
<h1 class="text-2xl font-extrabold mb-2">
|
||||
{{ $t("energy.elec_price_list") }}
|
||||
</h1>
|
||||
<div class="content-box-background border-info border text-base p-4">
|
||||
<span class="font-semibold text-info">說明:</span>
|
||||
本系統使用「標準型時間電價二段式」來試算電費。<br />
|
||||
本系統所提供之時間電價計算結果,係以未超過契約容量為前提所進行之估算,僅供用電分析與管理參考之用。
|
||||
</div>
|
||||
<ButtonGroup
|
||||
:items="items"
|
||||
:withLine="true"
|
||||
|
@ -46,7 +46,7 @@ const onOk = async () => {
|
||||
id="elec_modal"
|
||||
:title="props.formState?.id ? t('button.edit') : t('button.add')"
|
||||
:onCancel="onCancel"
|
||||
width="400"
|
||||
:width="400"
|
||||
>
|
||||
<template #modalContent>
|
||||
<form ref="form" class="mt-5 w-full flex flex-wrap justify-between">
|
||||
|
@ -66,7 +66,7 @@ const onOk = async () => {
|
||||
id="floor_modal"
|
||||
:title="props.formState?.floor_guid ? t('button.edit') : t('button.add')"
|
||||
:onCancel="onCancel"
|
||||
width="400"
|
||||
:width="400"
|
||||
>
|
||||
<template #modalContent>
|
||||
<form ref="form" class="mt-5 w-full flex flex-wrap justify-between">
|
||||
|
@ -95,6 +95,10 @@ const PointsColumns = computed(() => [
|
||||
title: t("setting.hide_point"),
|
||||
key: "is_link",
|
||||
},
|
||||
{
|
||||
title: t("setting.hide_switch"),
|
||||
key: "show_event_switch_btn",
|
||||
},
|
||||
{
|
||||
title: t("assetManagement.operation"),
|
||||
key: "operation",
|
||||
@ -256,6 +260,9 @@ watch(
|
||||
<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 === 'show_event_switch_btn'">
|
||||
{{ record.show_event_switch_btn === true ? 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"
|
||||
|
@ -102,6 +102,10 @@ watch(
|
||||
);
|
||||
|
||||
const onOk = async () => {
|
||||
if (!schemaName.value) {
|
||||
openToast("error", t("msg.schema_name_required"), "#MQTT_Parse_item");
|
||||
return;
|
||||
}
|
||||
const points = formStates.value.map((state) => ({
|
||||
PointOrg: state.PointOrg,
|
||||
PointSys: props.pointsData.find((point) => point.id === state.item_id)
|
||||
@ -181,13 +185,14 @@ const onCancel = () => {
|
||||
type="text"
|
||||
v-model.text="schemaName"
|
||||
class="input border-info focus-within:border-info"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<table class="table w-1/2 mt-2">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{$t("setting.IoT_point_structure")}}</th>
|
||||
<th>{{$t("setting.system_point_name")}}</th>
|
||||
<th>{{ $t("setting.IoT_point_structure") }}</th>
|
||||
<th>{{ $t("setting.system_point_name") }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
@ -21,6 +21,9 @@ const formState = ref({
|
||||
decimals: 0,
|
||||
is_bool: 0,
|
||||
is_link: 0,
|
||||
show_event_switch_btn: false,
|
||||
event_switch_on_message: "",
|
||||
event_switch_off_message: "",
|
||||
});
|
||||
let schema = ref(
|
||||
yup.object({
|
||||
@ -42,10 +45,16 @@ const onOk = async () => {
|
||||
decimals: Number(values.decimals),
|
||||
is_bool: Number(values.is_bool),
|
||||
is_link: Number(values.is_link),
|
||||
event_switch_on_message: values.show_event_switch_btn
|
||||
? values.event_switch_on_message
|
||||
: "",
|
||||
event_switch_off_message: values.show_event_switch_btn
|
||||
? values.event_switch_off_message
|
||||
: "",
|
||||
});
|
||||
if (res.isSuccess) {
|
||||
props.getData(props.variable_id);
|
||||
onCancel();
|
||||
props.getData(props.variable_id);
|
||||
} else {
|
||||
openToast("error", res.msg, "#point_list_item");
|
||||
}
|
||||
@ -151,6 +160,43 @@ watch(
|
||||
>
|
||||
<template #topLeft>{{ $t("setting.hide_point") }}</template>
|
||||
</RadioGroup>
|
||||
|
||||
<RadioGroup
|
||||
class="my-2"
|
||||
name="show_event_switch_btn"
|
||||
:value="formState"
|
||||
:items="[
|
||||
{
|
||||
key: 1,
|
||||
value: true,
|
||||
title: $t('alert.yes'),
|
||||
},
|
||||
{
|
||||
key: 0,
|
||||
value: false,
|
||||
title: $t('alert.no'),
|
||||
},
|
||||
]"
|
||||
:required="true"
|
||||
>
|
||||
<template #topLeft>{{ $t("setting.hide_switch") }}</template>
|
||||
</RadioGroup>
|
||||
<Textarea
|
||||
v-if="formState.show_event_switch_btn"
|
||||
:value="formState"
|
||||
name="event_switch_on_message"
|
||||
class="w-full my-2"
|
||||
>
|
||||
<template #topLeft>{{ $t("setting.switch_on_message") }}</template>
|
||||
</Textarea>
|
||||
<Textarea
|
||||
v-if="formState.show_event_switch_btn"
|
||||
:value="formState"
|
||||
name="event_switch_off_message"
|
||||
class="w-full my-2"
|
||||
>
|
||||
<template #topLeft>{{ $t("setting.switch_off_message") }}</template>
|
||||
</Textarea>
|
||||
</form>
|
||||
</template>
|
||||
<template #modalAction>
|
||||
|
@ -60,7 +60,7 @@ const onOk = async () => {
|
||||
id="company_modal"
|
||||
:title="props.formState?.id ? t('button.edit') : t('button.add')"
|
||||
:onCancel="onCancel"
|
||||
width="710"
|
||||
:width="710"
|
||||
>
|
||||
<template #modalContent>
|
||||
<form ref="form" class="mt-5 w-full flex flex-wrap justify-between">
|
||||
|
95
src/views/setting/components/ViewModeSetting.vue
Normal file
95
src/views/setting/components/ViewModeSetting.vue
Normal file
@ -0,0 +1,95 @@
|
||||
<script setup>
|
||||
import { onMounted, ref, inject, computed } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { posttDashboard2D3D } from "@/apis/dashboard";
|
||||
import useBuildingStore from "@/stores/useBuildingStore";
|
||||
import { tr } from "date-fns/locale";
|
||||
|
||||
const { openToast, cancelToastOpen } = inject("app_toast");
|
||||
const buildingStore = useBuildingStore();
|
||||
const { t } = useI18n();
|
||||
const FILE_BASEURL = import.meta.env.VITE_FILE_API_BASEURL;
|
||||
const form = ref(null);
|
||||
|
||||
const formState = ref({
|
||||
showForgeArea: buildingStore.showForgeArea,
|
||||
lorf: buildingStore.previewImageExt
|
||||
? [
|
||||
{
|
||||
src: `${FILE_BASEURL}/upload/setting/previewImage/${buildingStore.selectedBuilding.building_guid}${buildingStore.previewImageExt}`,
|
||||
name: `${buildingStore.selectedBuilding.building_guid}`,
|
||||
ext: `${buildingStore.previewImageExt}`,
|
||||
},
|
||||
]
|
||||
: [],
|
||||
});
|
||||
|
||||
const updateFileList = (files) => {
|
||||
formState.value.lorf = files;
|
||||
};
|
||||
|
||||
const onShowForgeAreaChange = (e) => {
|
||||
formState.value.showForgeArea = e.target.checked;
|
||||
};
|
||||
|
||||
const onOk = async () => {
|
||||
const formData = new FormData();
|
||||
formData.delete("file");
|
||||
formData.append("buildingId", buildingStore.selectedBuilding.building_guid);
|
||||
formData.append("is3DEnabled", formState.value.showForgeArea);
|
||||
if (formState.value.lorf && formState.value.lorf.length > 0) {
|
||||
formData.append("file", formState.value.lorf[0]);
|
||||
}else {
|
||||
formData.append("removePreviewImage", true);
|
||||
}
|
||||
const res = await posttDashboard2D3D(formData);
|
||||
if (res.isSuccess) {
|
||||
await buildingStore.fetchDashboard2D3D(
|
||||
buildingStore.selectedBuilding.building_guid
|
||||
);
|
||||
openToast("success", t("msg.edit_successfully"));
|
||||
// 更新本地狀態
|
||||
formState.value.showForgeArea = buildingStore.showForgeArea;
|
||||
formState.value.lorf = buildingStore.previewImageExt
|
||||
? [ {
|
||||
src: `${FILE_BASEURL}/upload/setting/previewImage/${buildingStore.selectedBuilding.building_guid}/${buildingStore.previewImageExt}`,
|
||||
name: `${buildingStore.selectedBuilding.building_guid}`,
|
||||
ext: `${buildingStore.previewImageExt}`,
|
||||
},]
|
||||
: [];
|
||||
}else{
|
||||
openToast("error", res.msg);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex justify-start items-center mt-10 mb-5">
|
||||
<h3 class="text-xl mr-5">2D / 3D 顯示設定 :</h3>
|
||||
</div>
|
||||
<div class="flex gap-5">
|
||||
<span class="text-lg">3D 模型顯示 : </span>
|
||||
<input
|
||||
type="checkbox"
|
||||
class="toggle toggle-lg toggle-success"
|
||||
name="showForgeArea"
|
||||
:checked="formState.showForgeArea"
|
||||
@change="onShowForgeAreaChange"
|
||||
/>
|
||||
</div>
|
||||
<Upload
|
||||
class="mt-2 mb-5 max-w-[600px] w-full"
|
||||
name="oriFile"
|
||||
:fileList="formState.lorf"
|
||||
:getFileList="updateFileList"
|
||||
:multiple="false"
|
||||
formats="png、jpg"
|
||||
>
|
||||
<template #topLeft>首頁2D圖上傳 :</template>
|
||||
</Upload>
|
||||
<button type="submit" class="btn btn-outline-success" @click.prevent="onOk">
|
||||
{{ $t("button.submit") }}
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<style lang="css" scoped></style>
|
@ -1,6 +1,6 @@
|
||||
<script setup>
|
||||
import { RouterView, useRoute } from "vue-router";
|
||||
import { computed, watch, provide, ref, onMounted, onBeforeUnmount } from "vue";
|
||||
import { computed, watch, provide, ref, onMounted, onUnmounted } from "vue";
|
||||
import SystemFloorBar from "./components/SystemFloorBar.vue";
|
||||
import SystemDeptBar from "./components/SystemDeptBar.vue";
|
||||
import useBuildingStore from "@/stores/useBuildingStore";
|
||||
@ -16,6 +16,7 @@ import SystemFloor from "./SystemFloor.vue";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
const FILE_BASEURL = import.meta.env.VITE_FILE_API_BASEURL;
|
||||
const buildingStore = useBuildingStore();
|
||||
|
||||
const statusList = computed(() => {
|
||||
@ -41,7 +42,7 @@ const floors = ref([]);
|
||||
const deptData = ref([]);
|
||||
const companyOptions = ref([]);
|
||||
const selected_dbid = ref([]);
|
||||
const showForgeArea = ref(true); // 控制2D/3D切換與內容顯示
|
||||
const imgBaseUrl = ref("");
|
||||
|
||||
const getFloors = async () => {
|
||||
const res = await getAssetFloorList();
|
||||
@ -137,6 +138,19 @@ watch(
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
watch(
|
||||
() => buildingStore.previewImageExt,
|
||||
(newValue) => {
|
||||
if (buildingStore.selectedBuilding && buildingStore.selectedBuilding.building_guid) {
|
||||
imgBaseUrl.value = buildingStore.previewImageExt
|
||||
? `${FILE_BASEURL}/upload/setting/previewImage/${buildingStore.selectedBuilding.building_guid}${newValue}`
|
||||
: "/build_img.jpg";
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
watch(
|
||||
() => deptData.value,
|
||||
(newVal) => {
|
||||
@ -171,7 +185,7 @@ const updateCurrentFloor = (floor) => {
|
||||
};
|
||||
|
||||
const realtimeData = ref([]);
|
||||
const timeId = ref(null);
|
||||
let timeId = null;
|
||||
const getAllDeviceRealtime = async () => {
|
||||
// 立即執行一次
|
||||
const fetchData = async () => {
|
||||
@ -182,15 +196,22 @@ const getAllDeviceRealtime = async () => {
|
||||
realtimeData.value = res.data;
|
||||
};
|
||||
await fetchData(); // 立即執行一次
|
||||
|
||||
if (timeId) {
|
||||
clearInterval(timeId);
|
||||
timeId = null;
|
||||
}
|
||||
// 然後設定每 10 秒更新一次
|
||||
timeId.value = setInterval(fetchData, 10000);
|
||||
timeId = setInterval(fetchData, 10 * 1000);
|
||||
};
|
||||
|
||||
watch(
|
||||
subscribeData,
|
||||
(newValue) => {
|
||||
timeId.value && clearInterval(timeId.value);
|
||||
console.log("subscribeData changed:", newValue);
|
||||
|
||||
if (timeId) {
|
||||
clearInterval(timeId);
|
||||
}
|
||||
newValue.length > 0 && getAllDeviceRealtime();
|
||||
},
|
||||
{ deep: true, immediate: true }
|
||||
@ -249,13 +270,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;
|
||||
@ -271,8 +292,11 @@ provide("system_selectedDevice", {
|
||||
deptData,
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
clearInterval(timeId.value);
|
||||
onUnmounted(() => {
|
||||
if (timeId) {
|
||||
clearInterval(timeId);
|
||||
timeId = null;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
@ -327,7 +351,7 @@ onBeforeUnmount(() => {
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-span-1 h-full flex flex-col justify-between">
|
||||
<template v-if="showForgeArea">
|
||||
<template v-if="buildingStore.showForgeArea">
|
||||
<SystemMode />
|
||||
<div class="h-full relative">
|
||||
<SystemFloor
|
||||
@ -354,7 +378,7 @@ onBeforeUnmount(() => {
|
||||
<div class="h-full relative">
|
||||
<img
|
||||
alt="build"
|
||||
src="/build_img.jpg"
|
||||
:src="imgBaseUrl"
|
||||
:class="
|
||||
twMerge(
|
||||
'absolute w-full h-full transition-opacity duration-300',
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user