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