Compare commits

...

25 Commits

Author SHA1 Message Date
4b210fa19f 修正 spriteIconUrl 的路徑,移除多餘的 /dist,並更新 useHeatmapBarStore 的 API 路徑設定,簡化為統一的 /config.json 2025-08-29 16:27:37 +08:00
e45ac790a6 更新 Dockerfile 中的 changelog,去掉環境變數 VITE_MQTT_BASEURL,新增 MQTT 相關 API,並新增總部帳戶管理初版;資產管理 : 過濾無效檔案、更新資產編輯和圖表資料來源的處理邏輯。 2025-08-29 14:36:50 +08:00
63bc0b1b58 Merge branch 'feature/headquartersSetting' into feature/dockerSetting 2025-08-29 10:22:20 +08:00
64f35db51b 更新環境變數設定,新增 MQTT 相關 API,並新增總部帳戶管理初版 2025-08-29 10:18:57 +08:00
2dfc2e5297 資產管理 : 過濾無效檔案、更新資產編輯和圖表資料來源的處理邏輯 2025-08-25 11:57:57 +08:00
21d15d39f4 修正 index.html 中 favicon 的插入方式,確保其在原始檔案中預設一行,並更新多個組件以使用 window.env 讀取環境變數 2025-08-22 11:16:38 +08:00
d0a9aec12a 更新 Dockerfile 和 docker-entrypoint.sh,動態替換 index.html 的標題與 favicon,改善社群爬蟲抓取效果 2025-08-21 17:26:33 +08:00
30a632fd0c Merge branch 'feature/headquartersSetting' into feature/dockerSetting 2025-08-21 14:09:03 +08:00
8243900f96 修正預覽2D圖片時的錯誤 | 登入UI修改 2025-08-21 14:07:36 +08:00
5d027dd085 動態設定網頁標題、favicon 和 logo,並更新預設圖片路徑 2025-08-20 13:26:18 +08:00
2c92242cb1 修正匯入的 apihandler 模組名稱為 apiHandler 2025-08-18 17:03:41 +08:00
42742d1063 Merge branch 'feature/headquartersSetting' into feature/dockerSetting 2025-08-18 16:35:15 +08:00
5ba844e307 調整圖片樣式 | 移除圖片的 object-cover 類別 2025-08-18 16:31:48 +08:00
3976540280 預設2d圖更新 | 系統小卡預設色號 | deleteBuilding() 2025-08-12 10:40:40 +08:00
3427058cd2 MQTT publish topic 名稱修改 |
系統監控switch功能 | 總部地圖功能
2025-08-06 13:48:04 +08:00
e05e83bb03 首頁UI | debug: 頁面重新整理時會回首頁 | sidebar 語言包 | 能源管理 : 增長率 多去年資料 2025-08-05 13:49:52 +08:00
73a76aca2e Merge branch 'main' into feature/headquartersSetting 2025-08-04 15:17:39 +08:00
db5f15dfde 總部首頁 api 串接 | navbar語言包 2025-08-04 15:13:18 +08:00
1812ce2495 調整近30天能耗趨勢、環比能耗(假資料) 2025-07-28 14:53:40 +08:00
8d23b695c6 首頁初切版 2025-07-21 13:39:29 +08:00
f913f32915 Merge branch 'main' into feature/dockerSetting 2025-06-17 10:33:50 +08:00
eaaaf3ad9d docker變數設定 2025-06-11 18:12:07 +08:00
f88bd7fc4a Merge branch 'main' into feature/dockerSetting 2025-06-04 17:14:52 +08:00
8e2541f7c0 Merge branch 'main' into feature/dockerSetting 2025-06-02 09:45:17 +08:00
36f5ca4fd0 docker設定 2025-05-13 15:34:07 +08:00
86 changed files with 1956 additions and 288 deletions

6
.dockerignore Normal file
View File

@ -0,0 +1,6 @@
node_modules
.git
.gitignore
Dockerfile
docker-compose.yml
README.md

View File

@ -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"

View File

@ -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"

View File

@ -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
View 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
View 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 "$@"

View File

@ -10,6 +10,8 @@
/>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<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/ui/1.13.3/jquery-ui.js"></script>
<!-- <script type="text/javascript" src="/requirejs/config.js"></script> -->

7
package-lock.json generated
View File

@ -22,6 +22,7 @@
"echarts": "^5.4.3",
"jquery-ui": "^1.14.1",
"json-schema-generator": "^2.0.6",
"leaflet": "^1.9.4",
"mqtt": "^5.10.3",
"pinia": "^2.1.7",
"requirejs": "^2.3.6",
@ -3350,6 +3351,12 @@
"node": ">=0.10.0"
}
},
"node_modules/leaflet": {
"version": "1.9.4",
"resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.4.tgz",
"integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==",
"license": "BSD-2-Clause"
},
"node_modules/libphonenumber-js": {
"version": "1.10.60",
"resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.10.60.tgz",

View File

@ -24,6 +24,7 @@
"echarts": "^5.4.3",
"jquery-ui": "^1.14.1",
"json-schema-generator": "^2.0.6",
"leaflet": "^1.9.4",
"mqtt": "^5.10.3",
"pinia": "^2.1.7",
"requirejs": "^2.3.6",

BIN
public/CviLux_globalmap.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

BIN
public/CviLux_globalmap.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 552 KiB

View File

@ -10,7 +10,7 @@ import {
DELETE_ACCOUNT_USER_API,
} from "./api";
import instance from "@/util/request";
import apihandler from "@/util/apihandler";
import apihandler from "@/util/apiHandler";
export const getAccountUserList = async (search_condition = {}) => {
const res = await instance.post(GET_ACCOUNT_USERLIST_API, search_condition);

View File

@ -22,7 +22,7 @@ import {
POST_ALERT_MQTT_REFRESH
} from "./api";
import instance from "@/util/request";
import apihandler from "@/util/apihandler";
import apihandler from "@/util/apiHandler";
export const getAlertFormId = async (uuid) => {
const res = await instance.post(GET_ALERT_FORMID_API, uuid);

View File

@ -37,3 +37,5 @@ export const DELETE_ASSET_ELECTYPE_API = `/AssetManage/DeleteElecType`;
export const POST_ASSET_ELEC_SETTING_API = `/AssetManage/SaveAssetSetting`;
export const POST_ASSET_MQTT_PUBLISH_API = `/api/mqtt/publish`;
export const POST_MQTT_TOPIC_API = `api/Device/MQTTTopicTest`;
export const POST_MQTT_TOPIC_STOP_API = `api/Device/MQTTTopicTestStop`;

View File

@ -27,9 +27,11 @@ import {
DELETE_ASSET_ELECTYPE_API,
POST_ASSET_ELEC_SETTING_API,
POST_ASSET_MQTT_PUBLISH_API,
POST_MQTT_TOPIC_API,
POST_MQTT_TOPIC_STOP_API,
} from "./api";
import instance from "@/util/request";
import apihandler from "@/util/apihandler";
import apihandler from "@/util/apiHandler";
import { object } from "yup";
export const getAssetMainList = async (building_guid) => {
@ -359,3 +361,21 @@ export const postMQTTpublish = async ({ Topic, Payload }) => {
code: res.code,
});
};
export const postMqttTopic = async ({ iotTag, Topic }) => {
const res = await instance.post(POST_MQTT_TOPIC_API, { iotTag, Topic });
return apihandler(res.code, res.data, {
msg: res.msg,
code: res.code,
});
};
export const postMqttTopicStop = async ({ iotTag, Topic }) => {
const res = await instance.post(POST_MQTT_TOPIC_STOP_API, { iotTag, Topic });
return apihandler(res.code, res.data, {
msg: res.msg,
code: res.code,
});
};

View File

@ -4,3 +4,4 @@ export const DELETE_BUILDING_API = `/AssetManage/DeleteBuilding`;
export const GET_AUTHPAGE_API = `/api/GetUsrFroList`;
export const GET_SUBAUTHPAGE_API = `/api/Device/GetMainSub`;
export const GET_ALL_DEVICE_API = `/api/Device/GetAllDevice`;
export const GET_FUNCTION_LIST_API = `/api/function/get-function-list`;

View File

@ -5,9 +5,10 @@ import {
GET_AUTHPAGE_API,
GET_SUBAUTHPAGE_API,
GET_ALL_DEVICE_API,
GET_FUNCTION_LIST_API,
} from "./api";
import instance from "@/util/request";
import apihandler from "@/util/apihandler";
import apihandler from "@/util/apiHandler";
export const getBuildings = async () => {
const res = await instance.post(GET_BUILDING_API);
@ -39,9 +40,9 @@ export const deleteBuildings = async (building_guid) => {
});
};
export const getAuth = async (lang) => {
const res = await instance.post(GET_AUTHPAGE_API, {
lang,
export const getAuth = async (building_id) => {
const res = await instance.get(GET_FUNCTION_LIST_API, {
params: { building_id },
});
return apihandler(res.code, res.data, {
msg: res.msg,
@ -50,7 +51,7 @@ export const getAuth = async (lang) => {
};
export const getAllSysSidebar = async (building_guid) => {
const res = await instance.post(GET_SUBAUTHPAGE_API, {building_guid});
const res = await instance.post(GET_SUBAUTHPAGE_API, { building_guid });
return apihandler(res.code, res.data, {
msg: res.msg,
code: res.code,

View File

@ -15,7 +15,7 @@ import {
POST_DASHBOARD_2D3DINFO_API
} from "./api";
import instance from "@/util/request";
import apihandler from "@/util/apihandler";
import apihandler from "@/util/apiHandler";
export const getDashboardInit = async (page_type = "SR") => {
const res = await instance.post(GET_DASHBOARD_INIT_API, {

View File

@ -1,6 +1,6 @@
export const GET_REALTIME_DIST_API = `/api/Energe/GetRealTimeDistribution`;
export const GET_ELECUSE_DAY_API = `/api/Energe/GetElecUseDay`;
export const GET_TAI_POWER_API = `/api/Energe/GetTaipower`;
export const GET_TAI_POWER_API = `/api/energy-manager/power-usage`;
export const GET_SIDEBAR_API = `/api/GetSideBar`;
export const GET_SEARCH_API = `/api/Energe/GetFilter`;

View File

@ -15,7 +15,7 @@ import {
POST_TIME_ELEC_API,
} from "./api";
import instance, { fileInstance } from "@/util/request";
import apihandler from "@/util/apihandler";
import apihandler from "@/util/apiHandler";
import downloadExcel from "@/util/downloadExcel";
export const getRealTimeDist = async ({

View File

@ -1,6 +1,6 @@
import instance from "@/util/request";
import { GET_FORGETOKEN_API, GET_FORGEURN_API } from "./api";
import apihandler from "@/util/apihandler";
import apihandler from "@/util/apiHandler";
export const getUrn = async () => {
const res = await instance.post(GET_FORGEURN_API);

View File

@ -1,5 +1,5 @@
// 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 UPDATE_GRAPH_SIDEBAR_API = `/GraphManage/EditGraphManageTree`;

View 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`;

View 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,
});
}

View File

@ -1,5 +1,5 @@
// 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_POINT_API = `/api/History/GetAllDevPoi`;
export const GET_HISTORY_DATA_API = `/api/History/GetHistoryData`;

View File

@ -1,6 +1,6 @@
import { POST_LOGIN } from "./api";
import instance from "@/util/request";
import apihandler from "@/util/apihandler";
import apihandler from "@/util/apiHandler";
export async function Login({ account, password }) {
const res = await instance.post(POST_LOGIN, {

View File

@ -12,7 +12,7 @@ import {
DELETE_OPERATION_COMPANY_API,
} from "./api";
import instance from "@/util/request";
import apihandler from "@/util/apihandler";
import apihandler from "@/util/apiHandler";
import dayjs from "dayjs";
export const getOperationRecord = async ({

View File

@ -1,5 +1,5 @@
import instance from "@/util/request";
import apihandler from "@/util/apihandler";
import apihandler from "@/util/apiHandler";
import {
POST_SETTING_POINT_API,
GET_SETTING_TYPE_API,

View File

@ -1,3 +1,4 @@
export const GET_SYSTEM_FLOOR_LIST_API = `/api/Device/GetFloor`;
export const GET_SYSTEM_DEVICE_LIST_API = `/api/Device/GetDeviceList`;
export const GET_SYSTEM_REALTIME_API = `/api/Device/GetRealTimeData`;
export const GET_SYSTEM_DEVICE_POWER_TOGGLE_API = `/api/device-events/power-toggle`;

View File

@ -2,9 +2,10 @@ import {
GET_SYSTEM_FLOOR_LIST_API,
GET_SYSTEM_DEVICE_LIST_API,
GET_SYSTEM_REALTIME_API,
GET_SYSTEM_DEVICE_POWER_TOGGLE_API
} from "./api";
import instance from "@/util/request";
import apihandler from "@/util/apihandler";
import apihandler from "@/util/apiHandler";
export const getSystemFloors = async (building_tag, sub_system_tag) => {
const res = await instance.post(GET_SYSTEM_FLOOR_LIST_API, {
@ -42,3 +43,16 @@ export const getSystemRealTime = async (device_list) => {
code: res.code,
});
};
export const toggleDevicePower = async ({topic_publish, device_item_id,new_value}) => {
const res = await instance.post(GET_SYSTEM_DEVICE_POWER_TOGGLE_API, {
topic_publish,
device_item_id,
new_value,
});
return apihandler(res.code, res.data, {
msg: res.msg,
code: res.code,
});
}

View File

@ -1,7 +1,7 @@
<script setup>
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);
let viewer = null;

View File

@ -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 filePath = `${FILE_BASEURL}/upload/forge/0.svf`;
await loadModel(viewer, filePath)

View File

@ -93,8 +93,8 @@ const createSprites = async (dataVizExtn) => {
const DataVizCore = Autodesk.DataVisualization.Core;
const viewableType = DataVizCore.ViewableType.SPRITE;
let spriteColor = new THREE.Color(0xffffff);
const BASEURL = import.meta.env.VITE_FILE_API_BASEURL;
const spriteIconUrl = `${BASEURL}/dist/hotspot.svg`;
const BASEURL = window.env?.VITE_FILE_API_BASEURL;
const spriteIconUrl = `${BASEURL}/hotspot.svg`;
const style = new DataVizCore.ViewableStyle(
viewableType,
spriteColor,

View File

@ -11,6 +11,7 @@ import AlarmDrawer from "@/components/alarm/AlarmDrawer.vue";
import NavbarLang from "./NavbarLang.vue";
import { twMerge } from "tailwind-merge";
const FILE_BASEURL = window.env?.VITE_FILE_API_BASEURL;
const user = ref("");
const menuShow = ref(true);
const router = useRouter();
@ -28,7 +29,7 @@ const toggleMenu = () => {
menuShow.value = !menuShow.value;
};
const src = import.meta.env.MODE === "production" ? "./logo.svg" : Logo;
const src = `${FILE_BASEURL}/upload/logo.png`;
const logout = () => {
document.cookie = "JWT-Authorization=; Max-Age=0";
@ -69,8 +70,7 @@ const logout = () => {
to="/dashboard"
class="rounded-lg pl-4 text-2xl font-bold text-white flex items-center"
>
<img :src="src" alt="logo" class="w-8 me-1" />
CviLux Group
<img :src="src" alt="logo" width="180" />
</router-link>
<NavbarBuilding />
</div>

View File

@ -1,11 +1,18 @@
<script setup>
import { onMounted } from "vue";
import { onMounted,watch } from "vue";
import useBuildingStore from "@/stores/useBuildingStore";
import { useRouter } from "vue-router";
const store = useBuildingStore();
const router = useRouter();
const selectBuilding = (bui) => {
store.selectedBuilding = bui; // selectedBuildingwatch
if (bui.is_headquarter == true) {
router.replace({ path: "/headquarters" });
} else {
router.replace({ path: "/dashboard" });
}
};
onMounted(() => {

View File

@ -9,7 +9,7 @@ import { useI18n } from "vue-i18n";
import { useRoute } from "vue-router";
import { twMerge } from "tailwind-merge";
const { locale } = useI18n();
const { locale, t } = useI18n();
const store = useUserInfoStore();
const buildingStore = useBuildingStore();
const route = useRoute();
@ -17,8 +17,8 @@ const openKeys = ref([]); // 追蹤當前打開的子菜單
const menu_array = ref([]);
const currentAuthCode = ref("");
const iniFroList = async () => {
const res = await getAuth(locale.value);
const iniFroList = async (building_id) => {
const res = await getAuth(building_id);
store.updateAuthPage(
res.data.map((d) =>
@ -72,23 +72,21 @@ watch(
(newVal) => {
if (newVal !== null) {
getSubMonitorPage(newVal.building_guid);
iniFroList(newVal.building_guid);
}
}
);
watch(locale, () => {
iniFroList();
});
onMounted(() => {
iniFroList();
});
</script>
<template>
<ul class="px-1 menu-box my-2">
<li class="flex flex-col items-center justify-center">
<router-link
:to="{ name: 'dashboard' }"
:to="{
name:
buildingStore.selectedBuilding?.is_headquarter === true
? 'headquarters'
: 'dashboard',
}"
class="flex lg:flex-col justify-center items-center btn-group text-white"
>
<font-awesome-icon
@ -96,7 +94,7 @@ onMounted(() => {
size="2x"
class="w-10 m-auto"
/>
<span>{{ $t("home") }}</span>
<span>{{ $t("navbar.home") }}</span>
</router-link>
</li>
<li
@ -132,11 +130,11 @@ onMounted(() => {
size="2x"
class="w-10 m-auto"
/>
<span>{{ page.subName }}</span>
<span>{{ $t(`navbar.${page.showView}`) }}</span>
</a>
<router-link
v-else
:to="page.navigate"
:to="`/` + page.showView"
type="link"
class="flex lg:flex-col justify-center items-center btn-group text-white"
>
@ -145,7 +143,7 @@ onMounted(() => {
size="2x"
class="w-10 m-auto"
/>
<span>{{ page.subName }}</span>
<span>{{ $t(`navbar.${page.showView}`) }}</span>
</router-link>
</li>
</ul>
@ -169,7 +167,7 @@ onMounted(() => {
<a-sub-menu
v-for="main in menu_array"
:key="main.main_system_tag"
:title="main.full_name"
:title="main.resource ? t(`navbar.${main.resource}`) : main.full_name"
v-if="menu_array.length > 0 && open"
>
<a-menu-item
@ -198,7 +196,7 @@ onMounted(() => {
},
}"
>
{{ sub.full_name }}
{{ sub.resource ? $t(`navbar.${sub.resource}`): sub.full_name }}
</router-link>
</a-menu-item>
</a-sub-menu>

View File

@ -13,6 +13,39 @@
"name": "名称",
"time": "时间"
},
"navbar": {
"home": "首页",
"sysMonBtnList": "系统监控",
"historyData": "历史资料",
"energyManagement": "能源管理",
"alert": "告警",
"operation": "运维管理",
"graphManagement": "图资管理",
"AssetManagement": "资产管理",
"accountManagement": "帐号管理",
"UserManagement": "帐号管理",
"Setting": "系统设定",
"energy_analysis": "能源分析",
"consumption_report": "用电报表",
"chart_analysis": "图表分析",
"historical_curve": "历史曲线",
"quick_metering": "快速抄表",
"electricity_classification": "用电分类",
"daily_report": "日报表",
"weekly_report": "周报表",
"monthly_report": "月报表",
"annual_report": "年报表",
"setting":"系统设置",
"device":"设置设置",
"department": "部门",
"settings_2d_3d": "2D/3D 显示设置",
"maintenance_vendor": "维修厂商",
"building": "厂区",
"floor": "楼层",
"demand": "电表",
"time_based_pricing": "时段电价",
"mqtt_result": "MQTT 结果"
},
"upload": {
"title": "选择一个文件或拖放到这里",
"description": "档案不超过 10MB",
@ -89,11 +122,11 @@
"reset_value": "复归值",
"edit_automatic_demand": "编辑自动需量",
"elec_bills": "今年电费累计(元)",
"interval_elec_charges": "区间电费(元)",
"interval_elec_charges": "本月电费(元)",
"year_carbon_emission": "今年碳排当量累计(公斤)",
"interval_carbon_emission": "区间碳排当量",
"interval_carbon_emission": "本月碳排当量",
"year_elec_consumption": "今年用电度数(kWh)",
"interval_elec_consumption": "区间用电度数(kWh)",
"interval_elec_consumption": "本月用电度数(kWh)",
"monthly_elec_consumption": "每月用电分析",
"monthly_carbon_emission_and_reduction": "每月碳排当量 (kgCO2e)",
"monthly_bill_power": "每月计费度数 (kWh)",
@ -155,7 +188,9 @@
"off_peak_contract": "离峰契约",
"variable_electricity_charge": "流动电费",
"saturday": "周六",
"sunday_and_off_peak_days": "周日及离峰日"
"sunday_and_off_peak_days": "周日及离峰日",
"past_elec_data": "去年±数值(百分比)",
"past_month_elec_data": "上月±数值(百分比)"
},
"alarm": {
"title": "显示警告",
@ -396,6 +431,7 @@
},
"msg": {
"sure_to_delete": "是否确认删除该项目?",
"is_headquarters": "该帐号为总部帐号,是否仍要删除该项目?",
"sure_to_delete_permanent": "是否确认永久删除该项目?",
"delete_success": "删除成功",
"delete_failed": "删除失败",

View File

@ -10,9 +10,42 @@
"in_otal": "筆資料",
"skip_to": "跳至",
"serial_number": "序號",
"name": "名",
"name": "",
"time": "時間"
},
"navbar": {
"home": "首頁",
"sysMonBtnList": "系統監控",
"historyData": "歷史資料",
"energyManagement": "能源管理",
"alert": "告警",
"operation": "運維管理",
"graphManagement": "圖資管理",
"AssetManagement": "資產管理",
"accountManagement": "帳號管理",
"UserManagement": "帳號管理",
"Setting": "系統設定",
"energy_analysis": "能耗分析",
"consumption_report": "用電報表",
"chart_analysis": "圖表分析",
"historical_curve": "歷史曲線",
"quick_metering": "快速抄表",
"electricity_classification": "用電分類",
"daily_report": "日報表",
"weekly_report": "周報表",
"monthly_report": "月報表",
"annual_report": "年報表",
"setting":"系統設定",
"device":"設備設定",
"department": "部門",
"settings_2d_3d": "2D/3D 顯示設定",
"maintenance_vendor": "維運廠商",
"building": "廠區",
"floor": "樓層",
"demand": "電表",
"time_based_pricing": "時段電價",
"mqtt_result": "MQTT 結果"
},
"upload": {
"title": "選擇一個文件或拖放到這裡",
"description": "檔案不超過 10MB",
@ -89,11 +122,11 @@
"reset_value": "復歸值",
"edit_automatic_demand": "編輯自動需量",
"elec_bills": "今年電費累計(元)",
"interval_elec_charges": "區間電費(元)",
"interval_elec_charges": "本月電費(元)",
"year_carbon_emission": "今年碳排當量累計(公斤)",
"interval_carbon_emission": "區間碳排當量",
"interval_carbon_emission": "本月碳排當量",
"year_elec_consumption": "今年用電度數(kWh)",
"interval_elec_consumption": "區間用電度數(kWh)",
"interval_elec_consumption": "本月用電度數(kWh)",
"monthly_elec_consumption": "每月用電分析",
"monthly_carbon_emission_and_reduction": "每月碳排當量 (kgCO2e)",
"monthly_bill_power": "每月計費度數 (kWh)",
@ -155,7 +188,9 @@
"off_peak_contract": "離峰契約",
"variable_electricity_charge": "流動電費",
"saturday": "週六",
"sunday_and_off_peak_days": "週日及離峰日"
"sunday_and_off_peak_days": "週日及離峰日",
"past_elec_data": "去年±數值(百分比)",
"past_month_elec_data": "上月±數值(百分比)"
},
"alarm": {
"title": "顯示警告",
@ -396,17 +431,18 @@
},
"msg": {
"sure_to_delete": "是否確認刪除該項目?",
"is_headquarters": "該帳號為總部帳號,是否仍要刪除該項目?",
"sure_to_delete_permanent": "是否確認永久刪除該項目?",
"delete_success": "刪除成功",
"delete_failed": "刪除失敗",
"mqtt_refresh": "重新設定成功",
"schema_name_required": "架構名稱欄位必填",
"incorrect_format":"格式不正確",
"send_successfully":"送出成功",
"edit_successfully":"修改成功"
"incorrect_format": "格式不正確",
"send_successfully": "送出成功",
"edit_successfully": "修改成功"
},
"setting": {
"electricity_meter":"電表",
"electricity_meter": "電表",
"MQTT_parse": "MQTT 解析",
"schema": "架構",
"point": "點位",

View File

@ -13,6 +13,39 @@
"name": "Name",
"time": "Time"
},
"navbar": {
"home": "Home",
"sysMonBtnList": "Monitoring",
"historyData": "History Data",
"energyManagement": "Energy",
"alert": "Alert",
"operation": "Maintenance",
"graphManagement": "Graph",
"AssetManagement": "Devices",
"accountManagement": "Account",
"UserManagement": "Account",
"Setting": "Setting",
"energy_analysis": "Energy Analysis",
"consumption_report": "Consumption Report",
"chart_analysis": "Chart Analysis",
"historical_curve": "Historical Curve",
"quick_metering": "Quick Metering",
"electricity_classification": "Electricity Classification",
"daily_report": "Daily Report",
"weekly_report": "Weekly Report",
"monthly_report": "Monthly Report",
"annual_report": "Annual Report",
"setting":"Settings",
"device":"Device",
"department": "Department",
"settings_2d_3d": "2D/3D Display Settings",
"maintenance_vendor": "Vendor",
"building": "Building",
"floor": "Floor",
"demand": "Demand",
"time_based_pricing": "Time-based Pricing",
"mqtt_result": "MQTT Result"
},
"upload": {
"title": "Select a file or drag and drop here",
"description": "File size cannot exceed 10MB",
@ -89,11 +122,11 @@
"reset_value": "Reset Value",
"edit_automatic_demand": "Edit automatic demand",
"elec_bills": "Total electricity bills this year (yuan)",
"interval_elec_charges": "Interval electricity charges (yuan)",
"interval_elec_charges": "Electricity charges for this month (yuan)",
"year_carbon_emission": "Cumulative carbon emission equivalent this year (kg)",
"interval_carbon_emission": "Interval carbon emission equivalent",
"interval_carbon_emission": "This month's carbon emission equivalent",
"year_elec_consumption": "This year's electricity consumption (kWh)",
"interval_elec_consumption": "Interval electricity consumption (kWh)",
"interval_elec_consumption": "This month's electricity consumption (kWh)",
"monthly_elec_consumption": "Monthly electricity consumption analysis",
"monthly_carbon_emission_and_reduction": "Monthly carbon emission equivalent (kgCO2e)",
"monthly_bill_power": "Monthly billing power (kWh)",
@ -155,7 +188,9 @@
"off_peak_contract": "Off-Peak Demand Charge",
"variable_electricity_charge": "Variable Electricity Charge",
"saturday": "Saturday",
"sunday_and_off_peak_days": "Sunday and Off-Peak Days"
"sunday_and_off_peak_days": "Sunday and Off-Peak Days",
"past_elec_data": "Last year ± value (percentage)",
"past_month_elec_data": "Last month ± value (percentage)"
},
"alarm": {
"title": "Warning",
@ -396,6 +431,7 @@
},
"msg": {
"sure_to_delete": "Are you sure to delete this item?",
"is_headquarters": "This account is a headquarters account. Are you sure you want to delete this item?",
"sure_to_delete_permanent": "Are you sure you want to permanently delete this item?",
"delete_success": "Delete successfully",
"delete_failed": "Delete failed",

View File

@ -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 GET_AUTHPAGE_API = `${BASEURL}/api/GetUsrFroList`;
export const GET_SUBAUTHPAGE_API = `${BASEURL}/api/Device/GetMainSub`;

View File

@ -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_FORGEURN_API = `${BASEURL}/api/Device/GetBuild`;

View File

@ -2,69 +2,49 @@ export const AUTHPAGES = [
{
authCode: "PF0",
icon: "home",
navigate: "/dashboard",
},
{
authCode: "PF1",
icon: "tv",
navigate: "/system",
},
{
authCode: "PF2",
icon: "chart-pie",
pageName: "energyManagement",
navigate: "/energyManagement",
},
{
authCode: "PF3",
icon: "chart-area",
navigate: "/historyData",
},
{
authCode: "PF4",
icon: "chart-line",
navigate: "/historyData",
},
{
authCode: "PF5",
icon: "bell",
pageName: "alert",
navigate: "/alert",
},
{
authCode: "PF6",
icon: "server",
pageName: "operation",
navigate: "/operation",
},
{
authCode: "PF7",
icon: "image",
pageName: "graphManagement",
navigate: "/graphManagement",
},
{
authCode: "PF8",
icon: "user",
pageName: "accountManagement",
navigate: "/accountManagement",
},
{
authCode: "PF9",
icon: "database",
pageName: "AssetManagement",
navigate: "/assetManagement",
},
{
authCode: "PF10",
icon: "leaf",
pageName: "ProductSetting",
navigate: "/productSetting",
},
{
authCode: "PF11",
icon: "cog",
pageName: "Setting",
navigate: "/Setting",
},
];

View File

@ -18,6 +18,7 @@ export default function useForgeHeatmap() {
//create the heatmap
function getSensorValue(device, sensorType, pointData) {
console.log("heatmap", device, realtimeData.value);
const dev = realtimeData.value.find(
({ device_number }) => device_number === device.id
);

View File

@ -89,8 +89,8 @@ export default function useForgeSprite() {
const DataVizCore = Autodesk.DataVisualization.Core;
const viewableType = DataVizCore.ViewableType.SPRITE;
let spriteColor = new THREE.Color(0xffffff);
const BASEURL = import.meta.env.VITE_FILE_API_BASEURL;
const spriteIconUrl = `${BASEURL}/dist/hotspot.svg`;
const BASEURL = window.env?.VITE_FILE_API_BASEURL;
const spriteIconUrl = `${BASEURL}/hotspot.svg`;
const viewableData = new DataVizCore.ViewableData();
viewableData.spriteSize = 24; // Sprites as points of size 24 x 24 pixels
flatSubData.value?.forEach((d, index) => {

View File

@ -15,7 +15,7 @@ import App from "./App.vue";
import router from "./router";
import "virtual:svg-icons-register";
// 引入项目中的全部全局组件
import SvgIcon from "@/components/svgIcon.vue";
import SvgIcon from "@/components/SvgIcon.vue";
import library from "./fontawsomeIconRegister";
/* import font awesome icon component */

View File

@ -9,8 +9,11 @@ import AlertManagement from "@/views/alert/AlertManagement.vue";
import ProductSetting from "@/views/productSetting/ProductSetting.vue";
import EnergyManagement from "@/views/energyManagement/EnergyManagement.vue";
import SettingManagement from "@/views/setting/SettingManagement.vue";
import HeadquartersManagement from "@/views/headquarters/HeadquartersManagement.vue";
import UserManagement from "@/views/headquarters/HeadquartersAccountManagement.vue";
import Login from "@/views/login/Login.vue";
import useUserInfoStore from "@/stores/useUserInfoStore";
import useBuildingStore from "@/stores/useBuildingStore";
import useGetCookie from "@/hooks/useGetCookie";
import System from "@/views/system/System.vue";
import SystemFloor from "@/views/system/SystemFloor.vue";
@ -90,6 +93,16 @@ const router = createRouter({
name: "setting",
component: SettingManagement,
},
{
path: "/headquarters",
name: "headquarters",
component: HeadquartersManagement,
},
{
path: "/UserManagement",
name: "UserManagement",
component: UserManagement,
},
{
path: "/mytestfile/mjm",
name: "mytestfile",
@ -106,9 +119,11 @@ router.beforeEach(async (to, from, next) => {
const auth = useUserInfoStore();
const token = useGetCookie("JWT-Authorization");
const user_name = useGetCookie("user_name");
const buildingStore = useBuildingStore();
if ((authRequired && !token) || to.path === "/") {
auth.user.token = "";
buildingStore.deleteBuilding();
next({ path: "/login" });
} else if (!authRequired) {
document.cookie = "JWT-Authorization=; Max-Age=0";

View File

@ -84,8 +84,8 @@ const useBuildingStore = defineStore("buildingInfo", () => {
// 獲取2D、3D顯示與否
const fetchDashboard2D3D = async (BuildingId) => {
const res = await getDashboard2D3D(BuildingId);
showForgeArea.value = res.data.is3DEnabled;
previewImageExt.value = res.data.previewImageExt || "";
showForgeArea.value = res.data?.is3DEnabled || false;
previewImageExt.value = res.data?.previewImageExt || "";
};
// 清除localStorage建築物

View File

@ -10,10 +10,7 @@ const useHeatmapBarStore = defineStore("heatmap", () => {
const heatmapConfig = computed(() => allHeatMaps.value[route.query?.gas]);
const getConfig = async () => {
const api =
import.meta.env.MODE === "production"
? "/dist/config.json"
: "/config.json";
const api = "/config.json";
const res = await axios.get(api);
console.log(res);
allHeatMaps.value = res.data.heatmap;

View File

@ -4,7 +4,6 @@ import { ref } from "vue";
const useUserInfoStore = defineStore("userInfo", () => {
const user = ref({
token: "",
expires: 0,
user_name:"",
});

View File

@ -1,4 +1,4 @@
const BASEURL = import.meta.env.VITE_API_BASEURL;
const BASEURL = window.env?.VITE_API_BASEURL;
export default function downloadExcel(res) {
let disposition = res.headers.get("Content-Disposition");

View File

@ -1,6 +1,6 @@
import useGetCookie from "@/hooks/useGetCookie";
import axios from "axios";
const BASEURL = import.meta.env.VITE_API_BASEURL;
const BASEURL = window.env?.VITE_API_BASEURL;
// --- 請求攔截器的共用邏輯 ---
const requestInterceptor = (config) => {

View File

@ -12,13 +12,23 @@ const { searchParams, changeParams } = useSearchParam();
const companyOptions = ref([]);
const iotSchemaOptions = ref([]);
const elecTypeOptions = ref([]);
const iotSchemaTag = ref(""); // IOT Schema tagIoT
const getCompany = async () => {
const res = await getOperationCompanyList();
companyOptions.value = res.data.map((d) => ({ ...d, key: d.id }));
};
const getIOTSchemaOptions = async (id) => {
const res = await getIOTSchema(Number(id));
iotSchemaOptions.value = res.data.map((d) => ({ ...d, key: d.id }));
const data = res.data || [];
iotSchemaOptions.value = data.map((d) => ({ ...d, key: d.id }));
// tagIoT使
if (data.length > 0 && data[0].tagIoT) {
iotSchemaTag.value = data[0].tagIoT;
} else {
iotSchemaTag.value = "";
}
};
const getElecType = async () => {
const res = await getElecTypeList();
@ -51,6 +61,7 @@ provide("asset_modal_options", {
elecTypeOptions,
departmentList,
floors,
iotSchemaTag
});
</script>

View File

@ -9,10 +9,8 @@ import dayjs from "dayjs";
import { useI18n } from "vue-i18n";
const { t } = useI18n();
const { openToast, cancelToastOpen } = inject("app_toast");
const { companyOptions, departmentList, floors } = inject(
"asset_modal_options"
);
const FILE_BASEURL = import.meta.env.VITE_FILE_API_BASEURL;
const { companyOptions, departmentList, floors } = inject("asset_modal_options");
const FILE_BASEURL = window.env?.VITE_FILE_API_BASEURL;
const { searchParams, changeParams } = useSearchParam();
const totalCoordinates = ref({});
@ -148,7 +146,9 @@ const edit = async (id) => {
// changeParams({ ...searchParams.value, main_id: record.id });
const res = await getAssetSingle(id);
if (res.isSuccess) {
res.data.oriFile = res.data.oriFile.map((file, index) => ({
res.data.oriFile = res.data.oriFile
.filter((file) => file.saveName)
.map((file, index) => ({
...file,
key: index,
src: file.file_url,

View File

@ -6,7 +6,9 @@ import AssetTableModalLeft from "./AssetTableModalLeft.vue";
import AssetTableModalRight from "./AssetTableModalRight.vue";
import useFormErrorMessage from "@/hooks/useFormErrorMessage";
import * as yup from "yup";
import { useI18n } from "vue-i18n";
const { t } = useI18n();
const { openToast } = inject("app_toast");
const { searchParams, changeParams } = useSearchParam();
@ -75,8 +77,11 @@ const onOk = async () => {
main_id: props.editRecord ? props.editRecord.main_id : 0,
});
if (res.isSuccess) {
openToast("success", t("msg.send_successfully"), "#asset_add_table_item");
props.getData();
setTimeout(() => {
closeModal();
}, 1000);
} else {
openToast("error", res.msg, "#asset_add_table_item");
}
@ -102,12 +107,20 @@ const closeModal = () => {
</script>
<template>
<button class="btn btn-sm btn-add mr-3" @click.stop.prevent="openModal" :disabled="!searchParams.subSys_id">
<button
class="btn btn-sm btn-add mr-3"
@click.stop.prevent="openModal"
:disabled="!searchParams.subSys_id"
>
<font-awesome-icon :icon="['fas', 'plus']" />{{ $t("button.add") }}
</button>
<Modal
id="asset_add_table_item"
:title="editRecord?.main_id ? $t('assetManagement.edit_device') : $t('assetManagement.add_device')"
:title="
editRecord?.main_id
? $t('assetManagement.edit_device')
: $t('assetManagement.add_device')
"
:onCancel="closeModal"
:width="1600"
>

View File

@ -8,7 +8,7 @@ import useUserInfoStore from "@/stores/useUserInfoStore";
import dayjs from "dayjs";
import { useI18n } from "vue-i18n";
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 { updateLeftFields, formErrorMsg, formState } = inject(
"asset_table_modal_form"

View File

@ -4,7 +4,7 @@ import { ref, computed, inject, watch, onMounted } from "vue";
import { useI18n } from "vue-i18n";
import Menu from "@/components/customUI/Menu.vue";
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 columns = computed(() => [
{
@ -32,7 +32,9 @@ const getMenuData = async () => {
const getData = async (id) => {
const res = await getGraphData(id);
if (res.isSuccess) {
dataSource.value = res.data.map((d) => ({ ...d, key: d.id }));
dataSource.value = res.data
.filter((d) => d.oriSavName)
.map((d) => ({ ...d, key: d.id }));
}
};

View File

@ -1,102 +1,126 @@
<script setup>
import { onMounted, ref, inject, watch, computed } from "vue";
import { useI18n } from "vue-i18n";
import { postMQTTpublish } from "@/apis/asset";
import { postMQTTpublish, postMqttTopic, postMqttTopicStop } from "@/apis/asset";
import mqtt from "mqtt";
import dayjs from "dayjs";
const { t } = useI18n();
const { openToast, cancelToastOpen } = inject("app_toast");
const { formState } = inject("asset_table_modal_form");
const BASEURL = import.meta.env.VITE_MQTT_BASEURL;
const { iotSchemaTag } = inject("asset_modal_options");
// MQTT
const mqttClient = ref(null); // MQTT
const receivedMessages = ref([]); //
const countdown = ref(60); // 60
const hasStartedCountdown = ref(false); //
let timer = null; //
let mqttInterval = null;
const mqttCardDataList = ref([]); //
const openModal = () => {
if (!mqttClient.value) {
connectMqtt();
}
const openModal = async () => {
mqtt_test.showModal();
startCountdown(); //
};
const connectMqtt = () => {
const topic = formState.value.topic || ""; //
const mqttHost = `${BASEURL}`;
const protocol = "wss"; // "ws" "wss"
mqttClient.value = mqtt.connect(mqttHost, {
protocol,
reconnectPeriod: 1000, //
username: "admin", // MQTT
password: "mjmadmin@99", // MQTT
port: 443,
//
try {
await postMqttTopic({
iotTag: iotSchemaTag?.value,
Topic: formState.value.topic,
});
// console.log(" postMqttTopic API ");
} catch (error) {
console.error("首次 postMqttTopic 發送失敗", error);
}
// 5
mqttInterval = setInterval(async () => {
try {
const res = await postMqttTopic({
iotTag: iotSchemaTag?.value,
Topic: formState.value.topic,
});
mqttClient.value.on("connect", () => {
console.log("MQTT 已連接");
if (topic) {
mqttClient.value.subscribe(topic, (err) => {
if (!err) {
console.log(`已訂閱主題: ${topic}`);
// console.log("postMqttTopic ", res?.data);
const payload = res?.data;
// payload
if (payload && payload.data && payload.time) {
const timeAlreadyExists = mqttCardDataList.value.some(
(item) => item.time === payload.time
);
if (!timeAlreadyExists) {
mqttCardDataList.value.unshift({
...payload.data,
time: payload.time,
});
//
if (!hasStartedCountdown.value) {
hasStartedCountdown.value = true;
startCountdown();
}
} else {
console.error("訂閱失敗: ", err);
// console.log("", payload.time);
}
});
} else {
// console.warn(" time");
}
});
mqttClient.value.on("message", (topic, message) => {
//
const now = dayjs(); // 使 dayjs()
const timestamp = now.format("YYYY-MM-DD HH:mm:ss");
receivedMessages.value.push({
topic,
message: message.toString(),
timestamp: timestamp,
});
clearInterval(timer); //
});
mqttClient.value.on("error", (err) => {
console.error("MQTT 連線錯誤: ", err);
});
} catch (error) {
console.error("postMqttTopic 呼叫失敗:", error);
}
}, 5000);
};
const startCountdown = () => {
if (timer) return; //
countdown.value = 60;
timer = setInterval(() => {
if (countdown.value > 0) {
if (countdown.value > 1) {
countdown.value--;
} else {
onCancel(); // 1 onCancel
clearInterval(timer);
timer = null;
onCancel(); // 60
}
}, 1000);
};
const onCancel = () => {
const onCancel = async () => {
//
receivedMessages.value = [];
mqttCardDataList.value = [];
countdown.value = 60;
hasStartedCountdown.value = false;
mqtt_test.close();
// MQTT
if (mqttClient.value) {
mqttClient.value.end();
mqttClient.value = null;
// API
try {
await postMqttTopicStop({
iotTag: iotSchemaTag?.value,
Topic: formState.value.topic,
});
} catch (error) {
console.error("postMqttTopicStop 發送失敗", error);
}
//
// API Interval
if (mqttInterval) {
clearInterval(mqttInterval);
mqttInterval = null;
}
// Timer
if (timer) {
clearInterval(timer);
timer = null; //
timer = null;
}
countdown.value = 60;
};
const onSubmit = async () => {
const Topic = formState.value.topic;
const Topic = formState.value.topic_publish;
let Payload = "";
try {
Payload = JSON.stringify(JSON.parse(formState.value.publish_message));
@ -130,7 +154,7 @@ const onSubmit = async () => {
</div>
<div class="flex flex-col col-span-2 border-t-gray-400 border-t py-5">
<Input :value="formState" name="topic">
<Input :value="formState" name="topic_publish">
<template #topLeft>MQTT publish topic</template>
</Input>
<Textarea :value="formState" name="publish_message">
@ -142,42 +166,69 @@ const onSubmit = async () => {
</button>
</div>
<Modal id="mqtt_test" title="MQTT Topic" :onCancel="onCancel" :width="400">
<Modal id="mqtt_test" title="MQTT Topic" :onCancel="onCancel" width="400">
<template #modalContent>
<!-- 顯示接收到的訊息 -->
<div v-if="receivedMessages.length > 0" class="overflow-y-auto h-96">
<div v-if="mqttCardDataList.length > 0" class="overflow-y-auto h-96 mt-4">
<ul>
<li
v-for="(message, index) in receivedMessages"
v-for="(item, index) in mqttCardDataList"
:key="index"
class="bg-base-200 rounded-md text-wrap shadow shadow-slate-400 p-4 my-2 me-2"
>
<strong class="text-base block text-info mb-2"
>{{ message.topic }} :</strong
<strong
class="text-base block text-info mb-2 flex justify-between items-center"
>
<p class="text-sm break-words">{{ message.message }}</p>
<p class="text-xs text-slate-200 pt-2">
<span>
<FontAwesomeIcon :icon="['fas', 'clock']" class="me-1" />
{{ message.timestamp }}
{{ dayjs(item.time).format("YYYY-MM-DD HH:mm:ss") }}
</span>
<!-- 只在第一筆資料加上最新標籤 -->
<span
v-if="index === 0"
class="text-xs text-white bg-green-600 px-2 py-1 rounded"
>
New
</span>
</strong>
<!-- 動態顯示除了 time 以外的所有欄位 -->
<template v-for="[key, value] in Object.entries(item)" :key="key">
<p v-if="key !== 'time'" class="text-sm break-words">
<strong>{{ key }}</strong>{{ value }}
</p>
</template>
</li>
</ul>
</div>
<!-- 顯示 loading 和倒計時 -->
<p v-else class="text-center mt-20">
<!-- 顯示 loading 和倒計時只有沒資料才顯示 -->
<p v-if="mqttCardDataList.length === 0" class="text-center mt-20">
<Loading />
<br />
<span class="text-base">{{ countdown }} seconds</span>
<span class="text-base">Loading...</span>
</p>
</template>
<template #modalAction>
<div class="relative w-full flex justify-end items-center gap-12">
<!-- 資料出現後才顯示倒數計時置中顯示 -->
<div
v-if="mqttCardDataList.length > 0"
class="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 flex items-center gap-2 text-sm"
>
<span>Auto close in</span>
<span>{{ countdown }}s</span>
</div>
<button
type="reset"
class="btn btn-outline-success mr-2"
class="btn btn-outline-success"
@click.prevent="onCancel"
>
{{ t("button.cancel") }}
</button>
</div>
</template>
</Modal>
</template>

View File

@ -9,7 +9,7 @@ import { twMerge } from "tailwind-merge";
import { useI18n } from "vue-i18n";
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 { floors } = inject("asset_modal_options");
const { updateRightFields, formErrorMsg, formState } = inject(

View File

@ -127,8 +127,9 @@ const resetModalForm = () => {
};
};
const removeAccount = async (id) => {
openToast("warning", t("msg.sure_to_delete"), "body", async () => {
const removeAccount = async (id, notHeadquarters) => {
const message = notHeadquarters ? t("msg.sure_to_delete") : t("msg.is_headquarters");
openToast("warning", message, "body", async () => {
await cancelToastOpen();
const res = await delAccount(id);
if (res.isSuccess) {
@ -202,7 +203,7 @@ const removeAccount = async (id) => {
</button>
<button
class="btn btn-sm btn-error text-white"
@click.stop.prevent="() => removeAccount(record.userinfo_guid)"
@click.stop.prevent="() => removeAccount(record.userinfo_guid,record.canDeleted)"
>
{{ $t("button.delete") }}
</button>

View File

@ -7,7 +7,7 @@ import "yup-phone-lite";
import useFormErrorMessage from "@/hooks/useFormErrorMessage";
import { useI18n } from "vue-i18n";
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({
editRecord: Object,

View File

@ -17,7 +17,7 @@ const store = useBuildingStore();
const { items, changeActiveBtn, setItems, selectedBtn } = useActiveBtn();
let intervalId = null;
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 formState = ref({
building_guid: null,
@ -35,16 +35,23 @@ watch(
(newBuilding) => {
if (newBuilding) {
formState.value.building_guid = newBuilding.building_guid;
imgBaseUrl.value = store.previewImageExt
? `${FILE_BASEURL}/upload/setting/previewImage/${newBuilding.building_guid}${store.previewImageExt}`
: import.meta.env.MODE === "production"
? "dist/build_img.jpg"
: "/build_img.jpg";
}
},
{ immediate: true, deep: true }
);
watch(
[() => store.previewImageExt, () => formState.value.building_guid],
([newExt, buildingGuid]) => {
if (newExt && buildingGuid) {
imgBaseUrl.value = `${FILE_BASEURL}/upload/setting/previewImage/${buildingGuid}${newExt}`;
} else {
imgBaseUrl.value = `${FILE_BASEURL}/upload/build_img.jpg`;
}
},
{ immediate: true }
);
watch(
() => formState.value,
(newVal) => {
@ -139,7 +146,7 @@ onUnmounted(() => {
<template v-else>
<img
alt="build"
:src="imgBaseUrl || '/build_img.jpg'"
:src="imgBaseUrl"
class="area-img-box w-full h-[460px] block relative rounded-sm mt-3"
/>
</template>

View File

@ -5,7 +5,7 @@ import { twMerge } from "tailwind-merge";
import useBuildingStore from "@/stores/useBuildingStore";
const store = useBuildingStore();
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) => {
router.push({
name: "sub_system",

View File

@ -88,7 +88,7 @@ watch(
<th
class="text-base border text-white text-center bg-cyan-600 bg-opacity-30"
>
{{ $t("table.time") }}
{{ $t("operation.updated_time") }}
</th>
</tr>
</thead>

View File

@ -28,6 +28,7 @@ const {
} = useActiveBtn("multiple");
const taipower_data = ref([]);
const yearly_taipower_data = ref([]);
const carbonValue = ref(null);
const search_data = computed(() => {
return {
@ -41,9 +42,10 @@ const search_data = computed(() => {
const getData = async (value) => {
const res = await getTaipower(value);
if (res.isSuccess) {
taipower_data.value = res.data
? res.data.sort((a, b) => a.month.localeCompare(b.month))
taipower_data.value = res.data?.monthlyUsage
? res.data?.monthlyUsage.sort((a, b) => a.month.localeCompare(b.month))
: [];
yearly_taipower_data.value = res.data?.yearlyUsage;
}
};
@ -96,7 +98,12 @@ watch(
}
);
provide("energy_data", { taipower_data, search_data, carbonValue });
provide("energy_data", {
taipower_data,
yearly_taipower_data,
search_data,
carbonValue,
});
</script>
<template>

View File

@ -4,7 +4,7 @@ import { twMerge } from "tailwind-merge";
import { useI18n } from "vue-i18n";
const { t, locale } = useI18n();
const { taipower_data } = inject("energy_data");
const { taipower_data, yearly_taipower_data } = inject("energy_data");
const daysInMonth = (month) => {
const [year, monthNumber] = month.split("-");
@ -19,7 +19,10 @@ const calculateData = () => {
item.month.startsWith(currentYear)
);
const totalElecBills = filteredData.reduce((sum, item) => sum + item.costTotal, 0);
const totalElecBills = filteredData.reduce(
(sum, item) => sum + item.costTotal,
0
);
const latestMonthData = filteredData[filteredData.length - 1];
const latestMonth = latestMonthData ? latestMonthData.month : "";
const monthDays = latestMonth ? daysInMonth(latestMonth) : 0;
@ -37,29 +40,53 @@ const calculateData = () => {
);
return [
{ title: t("energy.elec_bills"), data: totalElecBills.toLocaleString() },
{
title: t("energy.elec_bills"),
data: totalElecBills.toLocaleString(),
past: yearly_taipower_data.value.costTotalGrowthPercent
? yearly_taipower_data.value.costTotalGrowthPercent.toLocaleString()
: 0,
},
{
title: t("energy.interval_elec_charges"),
time: monthTxt,
data: latestMonthData ? latestMonthData.costTotal.toLocaleString() : 0,
past:
latestMonthData && latestMonthData.costTotalGrowthPercent
? latestMonthData.costTotalGrowthPercent.toLocaleString()
: 0,
},
{
title: t("energy.year_carbon_emission"),
data: totalCarbonEmission.toLocaleString(),
past: yearly_taipower_data.value.carbonGrowthPercent
? yearly_taipower_data.value.carbonGrowthPercent.toLocaleString()
: 0,
},
{
title: t("energy.interval_carbon_emission"),
time: monthTxt,
data: latestMonthData ? latestMonthData.carbon.toLocaleString() : 0,
past:
latestMonthData && latestMonthData.carbonGrowthPercent
? latestMonthData.carbonGrowthPercent.toLocaleString()
: 0,
},
{
title: t("energy.year_elec_consumption"),
data: totalElecConsumption.toLocaleString(),
past: yearly_taipower_data.value.kWhGrowthPercent
? yearly_taipower_data.value.kWhGrowthPercent.toLocaleString()
: 0,
},
{
title: t("energy.interval_elec_consumption"),
time: monthTxt,
data: latestMonthData ? latestMonthData.kWh.toLocaleString() : 0,
past:
latestMonthData && latestMonthData.kWhGrowthPercent
? latestMonthData.kWhGrowthPercent.toLocaleString()
: 0,
},
];
};
@ -95,7 +122,9 @@ watch(
>
<div class="title">{{ item.title }}</div>
<div v-if="item.time" class="time">{{ item.time }}</div>
<div class="data">{{ item.data }}</div>
<div class="data">
<span>{{ item.data }}</span> / <span>{{ item.past }}</span>
</div>
</div>
</div>
</template>

View File

@ -10,7 +10,7 @@ import {
} from "@/apis/graph";
import { useI18n } from "vue-i18n";
const { t } = useI18n();
const BASEURL = import.meta.env.VITE_FILE_API_BASEURL;
const BASEURL = window.env?.VITE_FILE_API_BASEURL;
const props = defineProps({
updateEditRecord: Function,

View File

@ -8,7 +8,7 @@ import { useI18n } from "vue-i18n";
const { t } = useI18n();
const { openToast, cancelToastOpen } = inject("app_toast");
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(() => [
{

View 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>

View 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>

View 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>

View 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>

View 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>

View 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:
'&copy; <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>

View 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>

View 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>

View File

@ -1,13 +1,14 @@
<script setup>
import { onMounted, ref, watch, inject } from "vue";
import { Login } from "@/apis/login";
import Image from "@/assets/img/logo.svg";
import * as yup from "yup";
import useFormErrorMessage from "@/hooks/useFormErrorMessage";
import { useRouter } from "vue-router";
import useUserInfoStore from "@/stores/useUserInfoStore";
import useBuildingStore from "@/stores/useBuildingStore";
import { useI18n } from "vue-i18n";
const FILE_BASEURL = window.env?.VITE_FILE_API_BASEURL;
const { t } = useI18n();
const { openToast } = inject("app_toast");
const store = useUserInfoStore();
@ -30,15 +31,22 @@ const togglePasswordVisibility = () => {
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 value = await handleSubmit(schema, formState.value);
const res = await Login(value);
if (res.isSuccess) {
store.user = res.data;
localStorage.setItem("CviBuildingList", JSON.stringify(res.data.buildingIdList));
localStorage.setItem(
"CviBuildingList",
JSON.stringify(res.data.building_infos)
);
if (res.data.building_infos.map((b) => b.is_headquarter).includes(true)) {
router.replace({ path: "/headquarters" });
} else {
router.replace({ path: "/dashboard" });
}
} else {
openToast("error", res.msg);
}
@ -47,18 +55,17 @@ const doLogin = async () => {
<template>
<div
class="absolute top-10 left-1/2 -translate-x-1/2 z-20 border-4 rounded-3xl p-5 bg-white bg-opacity-60"
class="absolute top-10 left-1/2 -translate-x-1/2 z-20 border-2 border-slate-200/40 shadow-lg shadow-slate-100/20 rounded-3xl p-5 bg-gradient-to-br from-slate-600 to-slate-800 bg-opacity-60"
>
<div class="flex flex-col items-start w-96">
<div
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" />
CviLux Group
<img :src="imageSrc" alt="logo" width="250" />
</div>
<div class="w-full flex flex-col items-end my-2">
<div class="w-full flex justify-between">
<label class="mr-2 text-2xl text-black" for="account">{{
<label class="mr-2 text-2xl text-slate-200" for="account">{{
$t("account")
}}</label>
<input
@ -74,7 +81,7 @@ const doLogin = async () => {
</div>
<div class="w-full flex flex-col items-end my-2">
<div class="w-full flex justify-between relative">
<label class="mr-2 text-2xl text-black" for="password">{{
<label class="mr-2 text-2xl text-slate-200" for="password">{{
$t("password")
}}</label>
<input

View File

@ -8,7 +8,7 @@ import useSearchParam from "@/hooks/useSearchParam";
import { useI18n } from "vue-i18n";
const { t } = useI18n();
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 { dataSource, openModal, updateEditRecord, search, tableLoading } =

View File

@ -12,7 +12,7 @@ import Select from "@/components/customUI/Select.vue";
import SearchSelect from "@/components/customUI/SearchSelect.vue";
import { useI18n } from "vue-i18n";
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({
editRecord: Object,

View File

@ -7,7 +7,7 @@ import { useI18n } from "vue-i18n";
import useBuildingStore from "@/stores/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 { openToast, cancelToastOpen } = inject("app_toast");

View File

@ -8,7 +8,7 @@ import { tr } from "date-fns/locale";
const { openToast, cancelToastOpen } = inject("app_toast");
const buildingStore = useBuildingStore();
const { t } = useI18n();
const FILE_BASEURL = import.meta.env.VITE_FILE_API_BASEURL;
const FILE_BASEURL = window.env?.VITE_FILE_API_BASEURL;
const form = ref(null);
const formState = ref({

View File

@ -16,7 +16,7 @@ import SystemFloor from "./SystemFloor.vue";
import { twMerge } from "tailwind-merge";
import dayjs from "dayjs";
const FILE_BASEURL = import.meta.env.VITE_FILE_API_BASEURL;
const FILE_BASEURL = window.env?.VITE_FILE_API_BASEURL;
const buildingStore = useBuildingStore();
const statusList = computed(() => {
@ -130,11 +130,6 @@ watch(
(newBuilding) => {
if (Boolean(newBuilding)) {
getData();
imgBaseUrl.value = buildingStore.previewImageExt
? `${FILE_BASEURL}/upload/setting/previewImage/${newBuilding.building_guid}${buildingStore.previewImageExt}`
: import.meta.env.MODE === "production"
? "dist/build_img.jpg"
: "/build_img.jpg";
}
},
{
@ -143,6 +138,18 @@ watch(
}
);
watch(
[() => buildingStore.previewImageExt, () => buildingStore.selectedBuilding?.building_guid],
([newExt, buildingGuid]) => {
if (newExt && buildingGuid) {
imgBaseUrl.value = `${FILE_BASEURL}/upload/setting/previewImage/${buildingGuid}${newExt}`;
} else {
imgBaseUrl.value = `${FILE_BASEURL}/upload/build_img.jpg`;
}
},
{ immediate: true }
);
watch(
() => deptData.value,
(newVal) => {
@ -184,7 +191,7 @@ const getAllDeviceRealtime = async () => {
const res = await getSystemRealTime(
subscribeData.value.map((d) => d.device_number)
);
console.log(res.data);
console.log("realtimeData",res.data);
realtimeData.value = res.data;
};
await fetchData(); //
@ -262,13 +269,13 @@ const getCurrentInfoModalData = async (e, position, value) => {
document.getElementById("system_info_modal").showModal();
};
const selectedDeviceRealtime = computed(
() =>
realtimeData.value?.find(
({ device_number }) =>
device_number === selectedDevice.value?.value?.device_number
)?.data
);
const selectedDeviceRealtime = computed(() => {
const deviceNumber = selectedDevice.value?.value?.device_number;
if (!deviceNumber) return [];
return realtimeData.value
.filter(item => item.device_number === deviceNumber && Array.isArray(item.data))
.flatMap(item => item.data.map(dataItem => ({ ...dataItem, topic_publish: item.topic_publish })));
});
const clearSelectedDeviceInfo = () => {
selectedDevice.value.value = null;

View File

@ -11,7 +11,7 @@ const { currentFloor, subscribeData, realtimeData } = inject("system_deviceList"
const { getCurrentInfoModalData, selected_dbid } = inject(
"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 sameOption = {

View File

@ -8,8 +8,7 @@ const { getCurrentInfoModalData, selected_dbid } = inject(
const { subscribeData, realtimeData } = inject("system_deviceList");
const { showData } = useSystemShowData();
const FILE_BASEURL = import.meta.env.VITE_FILE_API_BASEURL;
const FILE_BASEURL = window.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-AA003' || device.full_name === 'SmartSocket-AA004') return 'gray';
const state = getDeviceRealtimeState(device.device_number);
if (state === 'offnormal' || state === '') return device.device_close_color || 'gray';
return device.device_normal_color;
if (state === 'offnormal' || state === '') return device.device_close_color || '#9D9D9D';
return device.device_normal_color || '#009100';
};
//

View File

@ -1,8 +1,9 @@
<script setup>
import { ref, computed, inject, watch } from "vue";
import { useI18n } from "vue-i18n";
import { toggleDevicePower } from "@/apis/system";
import dayjs from "dayjs";
const { openToast } = inject("app_toast");
const { t } = useI18n();
const { selectedDevice, selectedDeviceRealtime } = inject(
"system_selectedDevice"
@ -13,7 +14,7 @@ const groupedData = computed(() => {
if (selectedDeviceRealtime?.value) {
selectedDeviceRealtime.value.forEach((record) => {
const { ori_device_number, time } = record;
const { ori_device_number, time, topic_publish } = record;
if (!grouped[ori_device_number]) {
grouped[ori_device_number] = {
@ -26,6 +27,8 @@ const groupedData = computed(() => {
if (record.point === d.points) {
grouped[ori_device_number].data.push({
...d,
device_item_id: record.device_item_id,
topic_publish: record.topic_publish,
value: record.value,
});
}
@ -36,9 +39,19 @@ const groupedData = computed(() => {
return grouped;
});
const togglePowerSwitch = (e, val) => {
const togglePowerSwitch = async (e, device_item_id, topic_publish) => {
const isChecked = e.target.checked;
console.log("Power Switch", e, val, "狀態:", isChecked);
const res = await toggleDevicePower({
device_item_id,
topic_publish,
new_value: isChecked,
});
if (res.isSuccess) {
openToast("success", t("msg.edit_successfully"), "#system_info_modal");
} else {
openToast("error", res.msg, "#system_info_modal");
}
};
</script>
@ -64,21 +77,23 @@ const togglePowerSwitch = (e, val) => {
</tr>
</thead>
<tbody>
<tr v-for="(group, index) in group.data" :key="index">
<tr v-for="(items, index) in group.data" :key="index">
<td class="border text-white text-lg text-center">
{{ group.full_name }}
{{ items.full_name }}
</td>
<td class="border text-white text-lg text-center">
<template v-if="group.full_name === 'Power Switch'">
<template v-if="items.full_name === 'Power Switch'">
<input
type="checkbox"
class="toggle toggle-success"
:checked="group.value"
@change="togglePowerSwitch($event, group.value)"
:checked="items.value"
@change="
togglePowerSwitch($event, items.device_item_id, items.topic_publish)
"
/>
</template>
<template v-else>
{{ group.value }}
{{ items.value }}
</template>
</td>
</tr>

View File

@ -2,7 +2,7 @@
import { computed, inject, watch, ref } from "vue";
import { useI18n } from "vue-i18n";
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 imgData = ref([]);

View File

@ -10,18 +10,9 @@ import path from "path";
export default defineConfig({
base: process.env.NODE_ENV === "production" ? "./" : "/",
build: {
outDir: process.env.NODE_ENV === "production" ? "../dist" : "./dist",
outDir: "./dist",
emptyOutDir: true,
},
server: {
proxy: {
'/upload': {
target: "https://ibms-cvilux.production.mjmtech.com.tw",
changeOrigin: true,
secure: false,
},
},
},
plugins: [
vue(),
Components({