Compare commits

..

1 Commits

Author SHA1 Message Date
a99af4d2d7 1.加入docker file設定
2.調整組件以使用 window.env 讀取環境變數
2025-09-03 14:52:47 +08:00
72 changed files with 430 additions and 1176 deletions

7
.dockerignore Normal file
View File

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

View File

@ -1,4 +0,0 @@
VITE_API_BASEURL = "https://ibms-Empower-api.production.mjmtech.com.tw"
VITE_FILE_API_BASEURL = "https://ibms-Empower.production.mjmtech.com.tw"
VITE_MQTT_BASEURL = "wss://mqttwss.mjm-staging.developers-homelab.net"
VITE_FORGE_BASEURL = "https://ibms-Empower.production.mjmtech.com.tw/dist"

View File

@ -1,4 +0,0 @@
VITE_API_BASEURL = "https://ibms-Empower-api.production.mjmtech.com.tw"
VITE_FILE_API_BASEURL = "https://ibms-Empower.production.mjmtech.com.tw"
VITE_MQTT_BASEURL = "wss://mqttwss.mjm-staging.developers-homelab.net"
VITE_FORGE_BASEURL = "https://ibms-Empower.production.mjmtech.com.tw/dist"

View File

@ -1,3 +0,0 @@
VITE_API_BASEURL = "http://220.132.206.5:8008"
VITE_FILE_API_BASEURL = "http://220.132.206.5:8085/file"
VITE_FORGE_BASEURL = "http://localhost:5173"

2
.gitattributes vendored
View File

@ -1,2 +0,0 @@
* text=auto
*.html text eol=lf

View File

@ -1,20 +0,0 @@
# Project
PROJ_NAME=proj_bims_ils-svc
# Network 網路環境
NET_TRAEFIK=net-traefik_svc
# Image: org/name
IMAGE_PROJ_NAME=proj_bims_ils
IMAGE_NAME=empower-front
TAG_VERSION=0.1.0
# Remote
REMOTE_URL=harbor.mjm-staging.developers-homelab.net
# ------------------------------------------------------------------------------
# Basic 基本配置
HOST_DOMAIN=ibms.mjmtech.com.tw
CUSTOMER_ID=empower1
WEB_TITLE=新創賦能

View File

@ -1,21 +0,0 @@
@echo off
cd /d "%~dp0"
setlocal enabledelayedexpansion
REM === 載入 .env 變數(忽略註解與空行) ===
for /f "usebackq tokens=1,* delims==" %%a in (".env") do (
if "%%a" neq "" (
if not "%%a"=="REM" (
set "%%a=%%b"
)
)
)
REM === 檢查變數 ========================
echo -----------------------------------
echo PROJ_NAME=!PROJ_NAME!
echo -----------------------------------
echo.
:: 啓動: 容器(containers)
docker-compose --project-name !PROJ_NAME! --env-file .env up -d

View File

@ -1,27 +0,0 @@
@echo off
cd /d "%~dp0"
setlocal enabledelayedexpansion
REM === 載入 .env 變數(忽略註解與空行) ===
for /f "usebackq tokens=1,* delims==" %%a in (".env") do (
if "%%a" neq "" (
if not "%%a"=="REM" (
set "%%a=%%b"
)
)
)
REM === 檢查變數 ========================
echo -----------------------------------
echo Check: IMAGE_NAME=!IMAGE_NAME!
echo Check: TAG_VERSION=!TAG_VERSION!
echo -----------------------------------
echo.
REM === 流程 ============================
:: 0. 移除舊 Image
docker rmi !IMAGE_NAME!:!TAG_VERSION!
:: 1. 打包 映像檔 ( -f: 文件位置(注:是相對於目錄) -t: 標簽 ..: 上一層作爲根目錄(.: 表示當前當作根目錄))
docker build -f Dockerfile -t !IMAGE_NAME!:!TAG_VERSION! ../../

View File

@ -1,47 +0,0 @@
@echo off
cd /d "%~dp0"
setlocal enabledelayedexpansion
REM === 載入 .env 變數(忽略註解與空行) ===
for /f "usebackq tokens=1,* delims==" %%a in (".env") do (
if "%%a" neq "" (
if not "%%a"=="REM" (
set "%%a=%%b"
)
)
)
REM === 檢查變數 ========================
echo -----------------------------------
echo Check: IMAGE_PROJ_NAME=!IMAGE_PROJ_NAME!
echo Check: IMAGE_NAME=!IMAGE_NAME!
echo Check: TAG_VERSION=!TAG_VERSION!
echo Check: REMOTE_URL=!REMOTE_URL!
echo -----------------------------------
echo.
REM === 設定目標 image tag ==============
set "LOCAL_TAG=!IMAGE_NAME!:!TAG_VERSION!"
set "REMOTE_TAG=!REMOTE_URL!/!IMAGE_PROJ_NAME!/!IMAGE_NAME!:!TAG_VERSION!"
REM === 流程 ============================
:: 1. 登入 遠端倉庫
echo.
echo Login...
docker login !REMOTE_URL!
:: 2. 標簽 標記映像檔
echo.
echo Tagging image...
docker tag !LOCAL_TAG! !REMOTE_TAG!
:: 3. 推送 映像檔
echo.
echo Pushing image...
docker push !REMOTE_TAG!
:: 4. 完成
echo Done.
pause

View File

@ -1,22 +0,0 @@
@echo off
cd /d "%~dp0"
setlocal enabledelayedexpansion
REM === 載入 .env 變數(忽略註解與空行) ===
for /f "usebackq tokens=1,* delims==" %%a in (".env") do (
if "%%a" neq "" (
if not "%%a"=="REM" (
set "%%a=%%b"
)
)
)
REM === 檢查變數 ========================
echo -----------------------------------
echo PROJ_NAME=!PROJ_NAME!
echo -----------------------------------
echo.
:: 停止: 容器(containers)
docker-compose stop

View File

@ -1,22 +0,0 @@
@echo off
cd /d "%~dp0"
setlocal enabledelayedexpansion
REM === 載入 .env 變數(忽略註解與空行) ===
for /f "usebackq tokens=1,* delims==" %%a in (".env") do (
if "%%a" neq "" (
if not "%%a"=="REM" (
set "%%a=%%b"
)
)
)
REM === 檢查變數 ========================
echo -----------------------------------
echo PROJ_NAME=!PROJ_NAME!
echo -----------------------------------
echo.
:: 刪除: 容器(containers)
docker-compose down

View File

@ -1,22 +0,0 @@
@echo off
cd /d "%~dp0"
setlocal enabledelayedexpansion
REM === 載入 .env 變數(忽略註解與空行) ===
for /f "usebackq tokens=1,* delims==" %%a in (".env") do (
if "%%a" neq "" (
if not "%%a"=="REM" (
set "%%a=%%b"
)
)
)
REM === 檢查變數 ========================
echo -----------------------------------
echo PROJ_NAME=!PROJ_NAME!
echo -----------------------------------
echo.
:: 刪除: 容器(containers) & 資料(volumes) & 網路(network)
docker-compose down -v

View File

@ -1,31 +0,0 @@
# 指令
# - 啓動容器: docker-compose --env-file .env up -d
# - 停止容器: docker-compose down
# - 移除容器: docker ps -a >> docker rm <containerId>
networks:
internal:
driver: bridge
traefik:
name: ${NET_TRAEFIK}
external: true
volumes:
upload:
services:
font:
environment:
# 動態設定 API 及檔案服務的 URL
VITE_API_BASEURL: https://${CUSTOMER_ID}-api.${HOST_DOMAIN}
VITE_FILE_API_BASEURL: https://${CUSTOMER_ID}-front.${HOST_DOMAIN}
VITE_APP_TITLE: ${WEB_TITLE}
#VITE_MQTT_BASEURL: wss://${CUSTOMER_ID}-mqtt.${HOST_DOMAIN}
volumes:
- upload:/usr/share/nginx/html/upload
networks:
- internal
- traefik

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}/favicon.ico\"|g" /usr/share/nginx/html/index.html
exec "$@"

View File

@ -10,8 +10,9 @@
/>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>新創賦能</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 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"

7
package-lock.json generated
View File

@ -21,7 +21,6 @@
"dayjs": "^1.11.10",
"echarts": "^5.4.3",
"flag-icons": "^7.2.3",
"hls.js": "^1.6.12",
"jquery-ui": "^1.14.1",
"json-schema-generator": "^2.0.6",
"mqtt": "^5.10.3",
@ -3057,12 +3056,6 @@
"integrity": "sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==",
"license": "MIT"
},
"node_modules/hls.js": {
"version": "1.6.12",
"resolved": "https://registry.npmjs.org/hls.js/-/hls.js-1.6.12.tgz",
"integrity": "sha512-Pz+7IzvkbAht/zXvwLzA/stUHNqztqKvlLbfpq6ZYU68+gZ+CZMlsbQBPUviRap+3IQ41E39ke7Ia+yvhsehEQ==",
"license": "Apache-2.0"
},
"node_modules/htmlparser2": {
"version": "3.10.1",
"resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.10.1.tgz",

View File

@ -22,7 +22,6 @@
"dayjs": "^1.11.10",
"echarts": "^5.4.3",
"flag-icons": "^7.2.3",
"hls.js": "^1.6.12",
"jquery-ui": "^1.14.1",
"json-schema-generator": "^2.0.6",
"mqtt": "^5.10.3",

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

@ -23,7 +23,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

@ -28,7 +28,7 @@ import {
POST_ASSET_ELEC_SETTING_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) => {

View File

@ -7,7 +7,7 @@ import {
GET_ALL_DEVICE_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);

View File

@ -13,7 +13,7 @@ import {
GET_DASHBOARD_ALARMOPERATION_INFO_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

@ -18,7 +18,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 getRealTimeData = 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

@ -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,5 +0,0 @@
// 開關 RTSP啟用/停用)
export const POST_SET_RTSP_ENABLE = `/api/rtsp/set-rtsp-enable`;
// 設定 SAMBA 儲存目錄
export const POST_SET_SAMBA_DIRECTORY = `/api/rtsp/set-samba-directory`;

View File

@ -1,29 +0,0 @@
import {
POST_SET_RTSP_ENABLE,
POST_SET_SAMBA_DIRECTORY,
} from "./api";
import instance from "@/util/request";
import apihandler from "@/util/apihandler";
/**
* 開關 RTSP
* Swagger: POST /api/rtsp/set-rtsp-enable
* body: { main_id: number, enable: boolean }
*/
export const setRtspEnable = async ({ main_id, enable }) => {
const res = await instance.post(POST_SET_RTSP_ENABLE, { main_id, enable });
return apihandler(res.code, res.data, { msg: res.msg, code: res.code });
};
/**
* 設定 SAMBA 儲存目錄
* Swagger: POST /api/rtsp/set-samba-directory
* body: { main_id: number, directory: string }
*/
export const setSambaDirectory = async ({ main_id, directory }) => {
const res = await instance.post(POST_SET_SAMBA_DIRECTORY, {
main_id,
directory,
});
return apihandler(res.code, res.data, { msg: res.msg, code: res.code });
};

View File

@ -7,7 +7,7 @@ import {
POST_MQTT_TOPIC_STOP_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, {

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

@ -106,7 +106,7 @@ watch(locale, () => {
<img src="/logo.svg" alt="logo" class="w-6 lg:w-8 me-1" />
新創賦能
</router-link>
<!-- <NavbarBuilding class="hidden lg:block ms-8" /> -->
<NavbarBuilding class="hidden lg:block ms-8" />
</div>
<div class="hidden flex-1 lg:block">
<NavbarItem

View File

@ -51,7 +51,7 @@
"indoor_chart": "室內",
"temperature": "温度",
"humidity": "湿度",
"no_data": "无数据",
"no_data":"无数据",
"alerts_data": "异常资料"
},
"history": {
@ -207,9 +207,6 @@
"completed": "已完成",
"worker_id": "工作人员编号",
"notice": "注意事项",
"video_storage_location": "告警影片儲存位置",
"copy": "复制",
"copied": "已复制!",
"result_description": "结果描述",
"upload_file": "上传文件",
"enable": "启用",
@ -421,21 +418,5 @@
"system_point_name": "系统点位名称",
"json_format_text": "请贴上 JSON 格式数据",
"json_click_text": "请在左侧输入JSON并点选转换按钮"
},
"rtsp": {
"title": "影像串流",
"start": "开始侦测",
"stop": "结束侦测",
"selectPath": "选择存储位置",
"selectDevice": "选择设备",
"pleaseSelectDevice": "请先选择设备",
"selectPathFirst": "请先选择存储文件夹",
"startSuccess": "已开始侦测…",
"startFail": "开始侦测失败,请稍后再试",
"stopSuccess": "已请求结束侦测…",
"stopFail": "结束侦测失败,请稍后再试",
"noPermission": "未获得写入权限,请重新选择文件夹并授权",
"selectFolderSuccess": "已选择文件夹:{name}",
"selectFolderFail": "选择文件夹失败,请再试一次"
}
}

View File

@ -207,9 +207,6 @@
"completed": "已完成",
"worker_id": "工作人員編號",
"notice": "注意事項",
"video_storage_location": "告警影片儲存位置",
"copy": "複製",
"copied": "已複製!",
"result_description": "結果描述",
"upload_file": "上傳檔案",
"enable": "啟用",
@ -421,21 +418,5 @@
"system_point_name": "系統點位名稱",
"json_format_text": "請貼上 JSON 格式數據",
"json_click_text": "請在左側輸入JSON並點選轉換按鈕"
},
"rtsp": {
"title": "影像串流",
"start": "開始偵測",
"stop": "結束偵測",
"selectPath": "選擇儲存位置",
"selectDevice": "選擇設備",
"pleaseSelectDevice": "請先選擇設備",
"selectPathFirst": "請先選擇儲存資料夾",
"startSuccess": "已開始偵測…",
"startFail": "開始偵測失敗,請稍後再試",
"stopSuccess": "已請求結束偵測…",
"stopFail": "結束偵測失敗,請稍後再試",
"noPermission": "沒有取得寫入權限,請重新選擇資料夾並允許",
"selectFolderSuccess": "已選擇資料夾:{name}",
"selectFolderFail": "選擇資料夾失敗,請再試一次"
}
}

View File

@ -51,7 +51,7 @@
"indoor_chart": "Indoor",
"temperature": "Temp.",
"humidity": "Hum.",
"no_data": "No data",
"no_data":"No data",
"alerts_data": "Abnormal data"
},
"history": {
@ -207,9 +207,6 @@
"completed": "Completed",
"worker_id": "Worker ID",
"notice": "Notice",
"video_storage_location": "video storage location",
"copy": "Copy",
"copied": "Copied!",
"result_description": "Result Description",
"upload_file": "Upload File",
"enable": "Enable",
@ -421,21 +418,5 @@
"system_point_name": "System Point Name",
"json_format_text": "Please paste JSON format data",
"json_click_text": "Please enter JSON on the left and click the conversion button"
},
"rtsp": {
"title": "Video Stream",
"start": "Start Detection",
"stop": "Stop Detection",
"selectPath": "Select Folder",
"selectDevice": "Select Device",
"pleaseSelectDevice": "Please select a device first",
"selectPathFirst": "Please select a folder first",
"startSuccess": "Detection started…",
"startFail": "Failed to start detection, please try again later",
"stopSuccess": "Detection stop requested…",
"stopFail": "Failed to stop detection, please try again later",
"noPermission": "No write permission. Please select a folder again and grant access",
"selectFolderSuccess": "Folder selected: {name}",
"selectFolderFail": "Failed to select folder, please try again"
}
}

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

@ -67,10 +67,4 @@ export const AUTHPAGES = [
pageName: "Setting",
navigate: "/Setting",
},
{
authCode: "PF12",
icon: "camera",
pageName: "rtsp",
navigate: "/rtsp",
},
];

View File

@ -63,8 +63,7 @@ import {
faSave,
faCrown,
faClock,
faCheckCircle,
faCamera
faCheckCircle
} from "@fortawesome/free-solid-svg-icons";
import { faCircle } from "@fortawesome/free-regular-svg-icons";
@ -131,7 +130,6 @@ library.add(
faCrown,
faClock,
faCheckCircle,
faCamera,
faCircle
);

View File

@ -1,83 +1,60 @@
// ---- styles ----
import "./assets/index.css";
import "./assets/main.css";
// import "./assets/table.css";
import "./assets/btn.css";
import "./assets/pagination.css";
// ---- Vue core ----
import { createApp, onErrorCaptured } from "vue";
import { createApp } from "vue";
import { createI18n } from "vue-i18n";
import { createPinia } from "pinia";
// ---- App / Router ----
import App from "./App.vue";
import router from "./router";
// ---- UI / Icons / Global comps ----
import Antd from "ant-design-vue";
import "virtual:svg-icons-register";
import SvgIcon from "@/components/SvgIcon.vue";
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
import library from "./fontawsomeIconRegister";
import "flag-icons/css/flag-icons.min.css";
// ---- Directives ----
import { focusPlugin } from "@/directives/focusPlugin";
import { draggable } from "@/directives/draggable";
// ---- i18n ----
import tw from "./config/tw.json";
import cn from "./config/cn.json";
import us from "./config/us.json";
import Antd from "ant-design-vue";
import { createPinia } from "pinia";
import App from "./App.vue";
import router from "./router";
import "virtual:svg-icons-register";
// 引入项目中的全部全局组件
import SvgIcon from "@/components/SvgIcon.vue";
import library from "./fontawsomeIconRegister";
import "flag-icons/css/flag-icons.min.css";
/* import font awesome icon component */
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
import { focusPlugin } from "@/directives/focusPlugin";
import { draggable } from "@/directives/draggable";
const messages = {
tw,
cn,
us,
};
const messages = { tw, cn, us };
const storedLanguage = localStorage.getItem("EmpowerLanguage") || "tw";
const i18n = createI18n({
legacy: false,
locale: storedLanguage,
fallbackLocale: "tw",
fallbackLocale: 'tw',
messages,
});
// 建立 App確保只 mount 一次)
// ===========================================
const app = createApp(App);
// 全域錯誤攔截:印出是哪個元件(含檔名)掛掉
app.config.errorHandler = (err, instance, info) => {
const name =
(instance && (instance.type?.name || instance.type?.__file)) ||
"(anonymous component)";
// 在 patch/update 階段若報 instance.update is not a function哪顆元件
console.error("[VueError]", name, info, err);
};
// (選用)開啟 devtools
// app.config.devtools = true;
// ===========================================
// 插件與全域元件
// ===========================================
app.use(createPinia());
app.use(router);
app.use(Antd);
app.use(i18n);
// 全域元件註冊(維持你的寫法)
// 组装成一个对象
const allGlobalComponents = { SvgIcon, FontAwesomeIcon };
app.use({
const globalComponent = {
install(app) {
Object.keys(allGlobalComponents).forEach((k) => {
app.component(k, allGlobalComponents[k]);
// 循环注册所有的全局组件
Object.keys(allGlobalComponents).forEach((componentName) => {
app.component(componentName, allGlobalComponents[componentName]);
});
},
});
// 指令
};
app.use(globalComponent);
app.use(focusPlugin);
app.use(draggable);
// ===========================================
// Mount保證只呼叫一次
// ===========================================
app.mount("#app");

View File

@ -56,12 +56,6 @@ const router = createRouter({
name: "assetManagement",
component: () => import("@/views/AssetManagement/AssetManagement.vue"),
},
{
path: "/rtsp",
name: "rtsp",
component: () => import("@/views/rtsp/Rtsp.vue"),
meta: { layout: "map", title: "rtsp" },
},
{
path: "/alert",
name: "alert",

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 instance = axios.create({
baseURL: BASEURL,

View File

@ -91,12 +91,12 @@ watch(selectedBtn, (newValue) => {
:getData="getMainSystems"
:formState="formState"
/>
<button
<!-- <button
@click.stop.prevent="isEditMode = !isEditMode"
class="btn btn-sm btn-outline-success"
>
{{ isEditMode ? t("button.stop_edit") : t("button.start_edit") }}
</button>
</button> -->
</div>
<ButtonConnectedGroup
:items="items"

View File

@ -51,9 +51,9 @@ const onReset = () => {
</script>
<template>
<button class="btn btn-sm btn-success" @click.stop.prevent="openModal">
<!-- <button class="btn btn-sm btn-success" @click.stop.prevent="openModal">
<font-awesome-icon :icon="['fas', 'plus']" />{{ $t("button.add") }}
</button>
</button> -->
<Modal
id="asset_add_main_item"
:title="

View File

@ -10,7 +10,7 @@ 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 FILE_BASEURL = window.env?.VITE_FILE_API_BASEURL;
const { searchParams, changeParams } = useSearchParam();
const totalCoordinates = ref({});

View File

@ -10,7 +10,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"
@ -155,7 +155,7 @@ watch(
<template #topLeft>IoT</template>
</Select>
</div>
<!-- <div class="flex items-center w-full" v-if="searchParams.mainSys_id == 26">
<div class="flex items-center w-full" v-if="searchParams.mainSys_id == 26">
<Select
:value="formState"
class="min-w-[180px] w-full"
@ -169,7 +169,7 @@ watch(
$t("energy.electricity_classification")
}}</template>
</Select>
</div> -->
</div>
<Input :value="formState" class="min-w-[180px] w-full" name="asset_number">
<template #topLeft>{{ $t("assetManagement.asset_number") }}</template>
<template #bottomLeft

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(() => [
{

View File

@ -7,7 +7,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

@ -1,13 +1,5 @@
<script setup>
import {
ref,
defineProps,
watch,
inject,
nextTick,
onMounted,
toRaw,
} from "vue";
import { ref, defineProps, watch, inject } from "vue";
import dayjs from "dayjs";
import { postOperationRecord } from "@/apis/alert";
import * as yup from "yup";
@ -15,33 +7,12 @@ 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,
});
/** ================= Debug Helpers ================= */
const debugLog = (label, payload) => {
try {
console.log(
`[AlertActionItem] ${label}:`,
JSON.parse(JSON.stringify(payload))
);
} catch (e) {
console.log(`[AlertActionItem] ${label}:`, payload);
}
};
// props editRecord
debugLog("props", props);
debugLog("initial props.editRecord", props?.editRecord);
onMounted(() => {
debugLog("onMounted props", props);
debugLog("onMounted props.editRecord", props?.editRecord);
});
const form = ref(null);
const dateItem = ref([
@ -93,43 +64,18 @@ const updateFileList = (files) => {
formState.value.lorf = files;
};
// ---------------------- API video_url ----------------------
// <Input name="videoLocation" :value="..."> value[name]
// videoLocation { videoLocation: string }
const videoLocation = ref({ videoLocation: "" });
const showTooltip = ref(false);
async function copyToClipboard() {
const text = videoLocation.value.videoLocation || "";
try {
await navigator.clipboard.writeText(text);
// tooltip
showTooltip.value = false;
await nextTick();
showTooltip.value = true;
setTimeout(() => {
showTooltip.value = false;
}, 1500);
} catch (err) {
console.error("複製失敗:", err);
}
}
const onOk = async () => {
const formData = new FormData(form.value);
formData.delete("oriFile");
(formState.value?.lorf ?? []).forEach((file, index) => {
formData.append(`lorf[${index}].id`, file?.id ? file.id : "");
//
if (!file?.id && file) {
formData.append(`lorf[${index}].file`, file);
}
formState.value?.lorf.forEach((file, index) => {
formData.append(`lorf[${index}].id`, file.id ? file.id : "");
formData.append(`lorf[${index}].file`, file.id ? null : file);
formData.append(
`lorf[${index}].save_file_name`,
file?.id ? file.save_file_name : ""
file.id ? file.save_file_name : ""
);
formData.append(`lorf[${index}].ori_file_name`, file?.name ?? "");
formData.append(`lorf[${index}].ori_file_name`, file.name);
});
formData.append(
@ -137,9 +83,9 @@ const onOk = async () => {
dayjs(dateItem.value[0].value).format("YYYY-MM-DD")
);
if (props.editRecord.id) formData.append("id", props.editRecord.id);
if (props.editRecord.uuid)
formData.append("error_code", props.editRecord.uuid);
props.editRecord.id && formData.append("id", props.editRecord.id);
props.editRecord.uuid && formData.append("error_code", props.editRecord.uuid);
formData.append("work_type", 2);
formData.append(
@ -150,7 +96,7 @@ const onOk = async () => {
);
try {
await handleSubmit(alertSchema, formState.value);
const value = await handleSubmit(alertSchema, formState.value);
const res = await postOperationRecord(formData);
if (res.isSuccess) {
search?.();
@ -177,36 +123,32 @@ const onCancel = () => {
description: "",
lorf: [],
};
//
videoLocation.value.videoLocation = "";
handleErrorReset();
updateEditRecord?.(null);
alert_action_item.close();
};
// props.editRecord -> formState / / /
watch(
() => props.editRecord,
(newVal) => {
debugLog("watch props.editRecord changed", newVal);
if (newVal) {
for (let [key, value] of Object.entries(newVal)) {
// formState
if (key in formState.value) {
formState.value[key] = value;
}
// start_time
if (key === "start_time") {
const d = value ? dayjs(value) : dayjs();
formState.value.start_time = d.format("YYYY-MM-DD");
dateItem.value[0].value = d; // dayjs
formState.value.start_time = value
? dayjs(value).format("YYYY-MM-DD")
: dayjs().format("YYYY-MM-DD");
dateItem.value[0].value = value;
}
//
if (key === "full_name") {
formState.value.fix_do = value ?? "";
formState.value.fix_do = value;
}
}
// API device_number
videoLocation.value.videoLocation =
newVal?.video_url ?? newVal?.videoUrl ?? newVal?.video_path ?? "";
debugLog("derived videoLocation", videoLocation.value.videoLocation);
}
},
{ immediate: true }
@ -221,11 +163,7 @@ watch(
width="710"
>
<template #modalContent>
<form
ref="form"
class="mt-5 w-full flex flex-wrap justify-between"
@submit.prevent
>
<form ref="form" class="mt-5 w-full flex flex-wrap justify-between">
<Input
v-if="formState && formState.formId"
class="my-2"
@ -348,48 +286,9 @@ watch(
</span></template
>
</Select>
<!-- 注意事項 -->
<Textarea :value="formState" name="notice" class="w-full my-2">
<template #topLeft>{{ $t("alert.notice") }}</template>
</Textarea>
<!-- 告警影片儲存位置-->
<div class="my-4 w-full">
<label class="text-lg">
{{ $t("alert.video_storage_location") }}
</label>
<div class="flex items-center gap-3">
<Input
class="flex-1"
name="videoLocation"
:value="videoLocation"
readonly
/>
<div class="relative inline-flex items-center">
<button
type="button"
class="btn btn-success"
@click.stop="copyToClipboard"
>
{{ $t("alert.copy") }}
</button>
<transition name="fade">
<span
v-if="showTooltip"
class="absolute left-full ml-4 top-1/2 -translate-y-1/2 text-white text-xs px-2 py-1 bg-gray-800 rounded shadow whitespace-nowrap"
role="status"
aria-live="polite"
>
{{ $t("alert.copied") }}
</span>
</transition>
</div>
</div>
</div>
<!-- 結果描述 -->
<Textarea :value="formState" name="description" class="w-full my-2">
<template #topLeft>{{ $t("alert.result_description") }}</template>
</Textarea>

View File

@ -14,7 +14,7 @@ import { computed, inject, ref, watch, onMounted, onUnmounted } from "vue";
import useBuildingStore from "@/stores/useBuildingStore";
import { getSystemDevices, getSystemRealTime } from "@/apis/system";
import DashboardRefrig from "./components/DashboardRefrig.vue";
const FILE_BASEURL = import.meta.env.VITE_FILE_API_BASEURL;
const FILE_BASEURL = window.env?.VITE_FILE_API_BASEURL;
const buildingStore = useBuildingStore();
const subscribeData = ref([]);
@ -100,8 +100,6 @@ const getData = async () => {
buying_date: device.buying_date,
created_at: device.created_at,
bgSize: 50,
is_rtsp: device.is_rtsp === true,
rtsp_url: device.rtsp_url || "",
},
];
});
@ -136,14 +134,20 @@ onUnmounted(() => {
<template>
<div class="flex flex-wrap justify-between">
<div
class="order-3 lg:order-1 w-full lg:w-1/4 min-h-screen flex flex-col justify-start item-center z-10 border-dashboard gap-12"
class="order-3 lg:order-1 w-full lg:w-1/4 min-h-screen flex flex-col justify-start z-10 border-dashboard gap-5"
>
<div class="flex flex-col gap-5">
<!-- 無資料時完整隱藏區塊不留空白 -->
<DashboardProduct
@visible-change="(v) => (productVisible = v)"
v-show="productVisible"
/>
<DashboardProductComplete
@visible-change="(v) => (productCompleteVisible = v)"
v-show="productVisible"
/>
<DashboardIndoor />
</div>
<div class="flex flex-col gap-5">
<DashboardRefrig />
</div>
<DashboardRefrig class="mb-10" />
</div>
<div
@ -152,19 +156,19 @@ onUnmounted(() => {
<DashboardFloorBar />
<DashboardEffectScatter :data="systemData" />
</div>
<!-- <div class="order-2 w-full lg:hidden my-3">
<div class="order-2 w-full lg:hidden my-3">
<DashboardSysCard :data="systemData" />
</div> -->
</div>
<div
class="order-last w-full lg:w-1/4 flex flex-col justify-start border-dashboard z-20 gap-12"
class="order-last w-full lg:w-1/4 flex flex-col justify-start border-dashboard z-20 gap-5"
>
<div class="flex flex-col gap-5">
<div>
<DashboardElectricity />
</div>
<div class="flex flex-col gap-5">
<div class="mt-10">
<DashboardEmission />
</div>
<div class="flex flex-col gap-5">
<div class="mt-10">
<DashboardAlert />
</div>
</div>

View File

@ -4,13 +4,12 @@ import EffectScatter from "@/components/chart/EffectScatter.vue";
import DashboardEffectScatterModal from "./DashboardEffectScatterModal.vue";
import useSearchParam from "@/hooks/useSearchParam";
import { computed, inject, ref, watch } from "vue";
import { twMerge } from "tailwind-merge";
import { useI18n } from "vue-i18n";
const { t } = useI18n();
const route = useRoute();
const { searchParams, changeParams } = useSearchParam();
const FILE_BASEURL = import.meta.env.VITE_FILE_API_BASEURL;
const FILE_BASEURL = window.env?.VITE_FILE_API_BASEURL;
const props = defineProps({
data: {
@ -92,48 +91,46 @@ const handleItemClick = (params) => {
watch(
[searchParams, () => asset_floor_chart.value, () => props.data],
([newValue, newChart, newData], [oldValue]) => {
if (
newValue.floor_id &&
newChart &&
Object.keys(newData || {}).length > 0
) {
const isFloorChanged = currentFloorId.value !== newValue.floor_id;
if (isFloorChanged) {
// SVG
console.log(
"Floor changed, updating chart with new SVG",
newValue.floor_id
);
currentFloorId.value = newValue.floor_id;
console.groupCollapsed("[FloorMap] watch fired");
console.log("floor_id =", newValue.floor_id);
console.log("chart ready =", !!newChart);
console.log("data keys =", Object.keys(newData || {}).length);
console.groupEnd();
const floorId = newValue.floor_id;
const chartReady = !!newChart;
// (1) / data SVG
if (floorId && chartReady && currentFloorId.value !== floorId) {
console.log("[FloorMap] load SVG for floor:", floorId);
currentFloorId.value = floorId;
newChart.updateSvg(
{
full_name: newValue.floor_id,
path: `${FILE_BASEURL}/upload/floor_map/${newValue.floor_id}.svg`,
full_name: floorId,
path: `${FILE_BASEURL}/upload/floor_map/${floorId}.svg`,
},
defaultOption(newValue.floor_id, currentIconData.value)
defaultOption(floorId, []) //
);
//
setTimeout(() => {
if (newChart.chart) {
newChart.chart.off("click"); //
newChart.chart.off("click");
newChart.chart.on("click", handleItemClick);
}
}, 100);
} else if (currentFloorId.value === newValue.floor_id && newChart.chart) {
// SVG
console.log("Data updated, refreshing chart data only");
}
// (2) SVG
if (
floorId &&
chartReady &&
Object.keys(newData || {}).length > 0 &&
newChart.chart
) {
console.log("[FloorMap] update series only for floor:", floorId);
newChart.chart.setOption(
{
series: defaultOption(newValue.floor_id, currentIconData.value)
.series,
},
{ series: defaultOption(floorId, currentIconData.value).series },
false,
true
);
}
}
},
{
immediate: true,

View File

@ -6,7 +6,10 @@ import { useI18n } from "vue-i18n";
import { twMerge } from "tailwind-merge";
const { t } = useI18n();
const currentTab = ref("desktop");
const changeOpenKey = (key) => {
currentTab.value = key;
};
const props = defineProps({
data: {
type: Object,
@ -14,16 +17,6 @@ const props = defineProps({
},
});
const isRtsp = computed(() => props.data?.is_rtsp === true);
const monitorUrl = computed(() => props.data?.rtsp_url || "");
// ------ tab / RTSP 使------
const currentTab = ref("desktop");
const changeOpenKey = (key) => {
currentTab.value = key;
};
const modal = ref(null);
// data modal
@ -31,23 +24,15 @@ watch(
() => props.data,
(newData) => {
if (newData) {
console.log("[props.data] =\n", JSON.stringify(props.data, null, 2));
dashboard_effectScatter_modal.showModal();
if (!isRtsp.value) currentTab.value = "desktop";
console.debug(
"[Modal Debug] is_rtsp:",
newData.is_rtsp,
"monitorUrl:",
monitorUrl.value
);
}
},
{ immediate: true }
);
// modal
// modal
const handleCancel = () => {
currentTab.value = "desktop";
currentTab.value = "desktop"; // desktop tab
dashboard_effectScatter_modal.close();
};
</script>
@ -62,13 +47,10 @@ const handleCancel = () => {
:draggable="true"
modalClass="max-h-[80vh]"
>
<!-- 標題列RTSP 不顯示分頁鈕 RTSP 顯示分頁鈕 -->
<template #modalTitle>
<div class="flex items-center justify-between">
<span>{{ props.data?.full_name }}</span>
<div class="flex items-center space-x-2">
<template v-if="!isRtsp">
<button
type="button"
class="text-base btn-link btn-text-without-border px-2"
@ -114,7 +96,7 @@ const handleCancel = () => {
"
/>
</button>
<button
<Button
type="button"
class="text-base btn-link btn-text-without-border px-2"
@click="() => changeOpenKey('chart')"
@ -128,9 +110,7 @@ const handleCancel = () => {
)
"
/>
</button>
</template>
</Button>
<button
type="button"
class="btn-link btn-text-without-border px-2"
@ -146,20 +126,7 @@ const handleCancel = () => {
</template>
<template #modalContent>
<!-- RTSP顯示 iframe -->
<div v-if="isRtsp" class="h-[60vh] py-4">
<div class="relative bg-black rounded h-full overflow-hidden">
<iframe
:src="monitorUrl"
class="absolute inset-0 w-full h-full"
allow="autoplay; fullscreen; picture-in-picture"
referrerpolicy="no-referrer"
></iframe>
</div>
</div>
<!-- RTSP顯示四分頁 -->
<div v-else class="space-y-4 py-4">
<div class="space-y-4 py-4">
<!-- Desktop Tab - 基本資訊 -->
<div v-if="currentTab === 'desktop'" class="grid grid-cols-1 gap-4">
<table
@ -207,6 +174,48 @@ const handleCancel = () => {
/>
</td>
</tr>
<!-- <tr v-if="props.data.Online_color" class="hover:bg-gray-600">
<td class="p-2 border">Online 顏色</td>
<td class="p-2 border">
<div class="flex items-center space-x-2">
<div
class="w-6 h-6 rounded border border-gray-300"
:style="{ backgroundColor: props.data.Online_color }"
></div>
<span class="text-gray-100 text-sm font-mono">{{
props.data.Online_color
}}</span>
</div>
</td>
</tr> -->
<!-- <tr v-if="props.data.Offline_color" class="hover:bg-gray-600">
<td class="p-2 border">Offline 顏色</td>
<td class="p-2 border">
<div class="flex items-center space-x-2">
<div
class="w-6 h-6 rounded border border-gray-300"
:style="{ backgroundColor: props.data.Offline_color }"
></div>
<span class="text-gray-100 text-sm font-mono">{{
props.data.Offline_color
}}</span>
</div>
</td>
</tr> -->
<!-- <tr v-if="props.data.Error_color" class="hover:bg-gray-600">
<td class="p-2 border">Error 顏色</td>
<td class="p-2 border">
<div class="flex items-center space-x-2">
<div
class="w-6 h-6 rounded border border-gray-300"
:style="{ backgroundColor: props.data.Error_color }"
></div>
<span class="text-gray-100 text-sm font-mono">{{
props.data.Error_color
}}</span>
</div>
</td>
</tr> -->
</tbody>
</table>
</div>
@ -248,35 +257,24 @@ const handleCancel = () => {
</td>
<td class="p-2 border">{{ props.data.device_coordinate }}</td>
</tr>
<tr>
<td class="p-2 border">
{{ $t("assetManagement.brand_and_modal") }}
</td>
<td class="p-2 border">
{{ props.data.brand }} / {{ props.data.device_model }}
</td>
<td class="p-2 border">{{ $t("assetManagement.brand_and_modal") }}</td>
<td class="p-2 border">{{ props.data.brand }} / {{ props.data.device_model }}</td>
</tr>
<tr>
<td class="p-2 border">
{{ $t("assetManagement.company_and_contact") }}
</td>
<td class="p-2 border">
{{ props.data.operation_name }} /
{{ props.data.operation_contact_person }}
</td>
<td class="p-2 border">{{ $t("assetManagement.company_and_contact") }}</td>
<td class="p-2 border">{{ props.data.operation_name }} / {{ props.data.operation_contact_person }}</td>
</tr>
<tr>
<td class="p-2 border">
{{ $t("assetManagement.buying_date") }}
</td>
<td class="p-2 border">{{ $t("assetManagement.buying_date") }}</td>
<td class="p-2 border">{{ props.data.buying_date }}</td>
</tr>
<tr>
<td class="p-2 border">
{{ $t("assetManagement.created_at") }}
</td>
<td class="p-2 border">{{ $t("assetManagement.created_at") }}</td>
<td class="p-2 border">{{ props.data.created_at }}</td>
</tr>
</tbody>
</table>
</div>
@ -286,9 +284,5 @@ const handleCancel = () => {
</template>
<style lang="scss" scoped>
/* 讓 Modal 內容能撐滿高度 */
:deep(.min-h-\[200px\]) {
min-height: 0 !important;
height: 100%;
}
/* 可以添加額外的樣式 */
</style>

View File

@ -8,7 +8,6 @@ import useBuildingStore from "@/stores/useBuildingStore";
const store = useBuildingStore();
const { t, locale } = useI18n();
const taipower_data = ref([]);
const elecUseDayData = ref([]);
const carbonValue = ref(null);
const carbonData = ref(null);
const search_data = computed(() => {
@ -108,7 +107,6 @@ watch(
JSON.stringify(newValue) !== JSON.stringify(oldValue)
) {
getData(newValue);
getElecUseDayData(newValue);
}
},
{

View File

@ -50,11 +50,10 @@ const defaultChartOption = ref({
tooltip: { trigger: "axis" },
legend: {
data: [],
top: 0, //
textStyle: { color: "#ffffff", fontSize: 12 },
textStyle: { color: "#ffffff", fontSize: 16 },
},
grid: {
top: "35%",
top: "10%",
left: "0%",
right: "0%",
bottom: "0%",
@ -204,7 +203,7 @@ onUnmounted(() => {
<h3 class="text-info text-xl text-center">
{{ $t("dashboard.indoor_chart") }}
</h3>
<div class="w-full flex justify-center items-center relative">
<div className="my-3 w-full flex justify-center relative">
<ButtonConnectedGroup
:items="items"
:onclick="(e, item) => changeActiveBtn(item)"

View File

@ -1,61 +1,150 @@
<script setup>
import LineChart from "@/components/chart/LineChart.vue";
import { SECOND_CHART_COLOR } from "@/constant";
import dayjs from "dayjs";
import { ref, watch, onUnmounted, computed } from "vue";
import { ref, watch, computed, onUnmounted } from "vue";
import useActiveBtn from "@/hooks/useActiveBtn";
import { getDashboardTemp } from "@/apis/dashboard";
import useSearchParams from "@/hooks/useSearchParam";
import useBuildingStore from "@/stores/useBuildingStore";
import { useI18n } from "vue-i18n";
import dayjs from "dayjs";
const { t, locale } = useI18n();
const { searchParams } = useSearchParams();
const buildingStore = useBuildingStore();
const timeoutTimer = ref(null); //
const { items, changeActiveBtn, setItems, selectedBtn } = useActiveBtn();
const allTempData = ref([]);
const currentOptionType = ref(1); // 1 = 2 =
const noData = ref(true); // API
const chartRef = ref(null);
// sysConfig
//
const timeoutTimer = ref(null);
const { items, changeActiveBtn, setItems, selectedBtn } = useActiveBtn();
const currentOptionType = ref(1); // 1: , 2:
const noData = ref(true); //
// getDashboardTemp chart
watch(
() => buildingStore.selectedBuilding?.building_guid,
async (guid) => {
if (timeoutTimer.value) clearInterval(timeoutTimer.value);
allTempData.value = [];
noData.value = true;
if (!guid) return;
if (guid) {
await buildingStore.getSysConfig(guid);
const showRefrigeration =
buildingStore.sysConfig?.value?.show_refrigeration;
if (showRefrigeration === false) {
noData.value = true; //
return;
noData.value = true; //
return; // getData
}
noData.value = false; //
await getData();
timeoutTimer.value = setInterval(getData, 60_000); //
noData.value = false; //
getData();
timeoutTimer.value = setInterval(getData, 60000); //
}
},
{ immediate: true }
);
//
// API
const getData = async () => {
const buildingGuid = buildingStore.selectedBuilding?.building_guid;
if (!buildingGuid) return;
try {
const res = await getDashboardTemp({
building_guid: buildingGuid,
tempOption: 2, // tempOption: 1
timeInterval: 1,
option: currentOptionType.value, // 1: 2:
});
const key = "冷藏"; // key
allTempData.value = res.isSuccess ? res.data?.[key] ?? [] : [];
noData.value = allTempData.value.length === 0;
} catch (e) {
console.error("getDashboardTemp error", e);
allTempData.value = [];
noData.value = true;
}
};
// watch
watch(
allTempData,
(newVal) => {
if (!newVal?.length || !other_real_temp_chart.value?.chart) return;
const firstValid = newVal.find((d) => d.data?.length);
if (!firstValid) return;
const sampledXAxis = sampleData(firstValid.data).map(({ time }) =>
dayjs(time).format("HH:mm:ss")
);
const allValues = newVal
.flatMap((d) => sampleData(d.data))
.map((d) => d.value)
.filter((v) => v != null);
if (!allValues.length) return;
const yMin = Math.floor(Math.min(...allValues)) - 1;
const yMax = Math.ceil(Math.max(...allValues)) + 1;
other_real_temp_chart.value.chart.setOption({
legend: {
data: newVal.map((d) => d.full_name),
},
xAxis: {
data: sampledXAxis,
},
yAxis: {
min: yMin,
max: yMax,
},
series: newVal.map((d, i) => ({
name: d.full_name,
type: "line",
data: sampleData(d.data).map(({ value }) => value),
showSymbol: false,
itemStyle: {
color: SECOND_CHART_COLOR[i % SECOND_CHART_COLOR.length],
},
})),
});
},
{ deep: true }
);
//
function sampleData(data = [], maxCount = 30) {
const len = data.length;
if (len <= maxCount) return data;
const sampled = [];
const step = (len - 1) / (maxCount - 1);
for (let i = 0; i < maxCount; i++) {
const index = Math.round(i * step);
sampled.push(data[index]);
}
return sampled;
}
// 使
const other_real_temp_chart = ref(null);
const defaultChartOption = ref({
tooltip: { trigger: "axis" },
tooltip: {
trigger: "axis",
},
legend: {
data: [],
top: 0, //
textStyle: { color: "#ffffff", fontSize: 12 },
textStyle: {
color: "#ffffff",
fontSize: 16,
},
},
grid: {
top: "35%",
top: "10%",
left: "0%",
right: "0%",
bottom: "0%",
@ -75,129 +164,40 @@ const defaultChartOption = ref({
series: [],
});
// /
const getData = async () => {
const buildingGuid = buildingStore.selectedBuilding?.building_guid;
if (!buildingGuid) return;
try {
const res = await getDashboardTemp({
building_guid: buildingGuid,
tempOption: 2, //
timeInterval: 1,
option: currentOptionType.value, // 1: 2:
});
console.log("[getDashboardTemp] 冷藏回傳:", res);
const key = "冷藏溫度"; // key
allTempData.value = res.isSuccess ? res.data?.[key] ?? [] : [];
noData.value = allTempData.value.length === 0;
console.log("[getDashboardTemp] allTempData", allTempData.value);
console.log("[getDashboardTemp] noData", noData.value);
} catch (e) {
console.error("getDashboardTemp error", e);
allTempData.value = [];
noData.value = true;
}
};
//
//
const buttonItems = computed(() => [
{ key: 1, title: t("dashboard.temperature"), active: true },
{ key: 2, title: t("dashboard.humidity"), active: false },
]);
//
watch(
() => locale.value,
() => setItems(buttonItems.value),
() => {
setItems(buttonItems.value);
},
{ immediate: true }
);
// /
// tab
watch(
selectedBtn,
async (newVal) => {
if ([1, 2].includes(newVal?.key)) {
currentOptionType.value = newVal.key;
if (buildingStore.sysConfig?.value?.show_refrigeration !== false) {
if (timeoutTimer.value) clearInterval(timeoutTimer.value);
await getData();
timeoutTimer.value = setInterval(getData, 60_000);
(newValue) => {
if (timeoutTimer.value) {
clearInterval(timeoutTimer.value);
}
if ([1, 2].includes(newValue?.key)) {
currentOptionType.value = newValue.key;
}
},
{ immediate: true, deep: true }
);
//
function sampleData(data = [], maxCount = 30) {
const len = data.length;
if (len <= maxCount) return data;
const sampled = [];
const step = (len - 1) / (maxCount - 1);
for (let i = 0; i < maxCount; i++) {
const index = Math.round(i * step);
sampled.push(data[index]);
}
return sampled;
}
//
watch(
allTempData,
(newVal) => {
const chart = chartRef.value?.chart;
if (!chart || !Array.isArray(newVal) || newVal.length === 0) return;
const firstValid = newVal.find(
(d) => Array.isArray(d.data) && d.data.length
);
if (!firstValid) return;
const sampledXAxis = sampleData(firstValid.data).map(({ time }) =>
dayjs(time).format("HH:mm:ss")
);
const allValues = newVal
.flatMap((d) => sampleData(d.data))
.map((d) => d?.value)
.filter((v) => typeof v === "number" && !Number.isNaN(v));
if (!allValues.length) return;
let yMin = Math.floor(Math.min(...allValues)) - 1;
let yMax = Math.ceil(Math.max(...allValues)) + 1;
if (yMin === yMax) {
yMin -= 1;
yMax += 1;
}
chart.setOption({
legend: { data: newVal.map((d) => d.full_name) },
xAxis: { data: sampledXAxis },
yAxis: { min: yMin, max: yMax },
series: newVal.map((d, i) => ({
name: d.full_name,
type: "line",
data: sampleData(d.data).map(({ value }) => value),
showSymbol: false,
itemStyle: {
color: SECOND_CHART_COLOR[i % SECOND_CHART_COLOR.length],
},
})),
});
},
{ deep: true }
);
//
//
onUnmounted(() => {
if (timeoutTimer.value) clearInterval(timeoutTimer.value);
if (timeoutTimer.value) {
clearInterval(timeoutTimer.value);
}
});
</script>
@ -205,27 +205,24 @@ onUnmounted(() => {
<h3 class="text-info text-xl text-center">
{{ $t("dashboard.refrig_chart") }}
</h3>
<div class="w-full flex justify-center relative">
<div className="my-3 w-full flex justify-center relative">
<ButtonConnectedGroup
:items="items"
:onclick="(e, item) => changeActiveBtn(item)"
/>
</div>
<div
v-if="noData"
class="text-center text-white text-lg min-h-[260px] flex items-center justify-center"
>
{{ $t("dashboard.no_data") }}
</div>
<LineChart
v-else
id="dashboard_refrigeration_temp"
v-if="!noData"
id="dashboard_other_real_temp"
class="min-h-[260px] max-h-fit"
:option="defaultChartOption"
ref="chartRef"
ref="indoorChartRef"
/>
</template>

View File

@ -2,7 +2,7 @@
import { ref, computed } from "vue";
import useSearchParam from "@/hooks/useSearchParam";
const { searchParams, changeParams } = useSearchParam();
const FILE_BASEURL = import.meta.env.VITE_FILE_API_BASEURL;
const FILE_BASEURL = window.env?.VITE_FILE_API_BASEURL;
const props = defineProps({
data: {
type: Object,

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

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

@ -1,276 +0,0 @@
<template>
<section class="min-h-[600px] h-screen">
<h1 class="text-2xl font-extrabold mb-2">{{ $t("rtsp.title") }}</h1>
<!-- Tabs選擇要顯示的攝影機 -->
<div class="flex items-center gap-4 mb-6">
<h2 class="text-lg font-bold whitespace-nowrap">
{{ $t("rtsp.selectDevice") }} :
</h2>
<ButtonConnectedGroup
:items="items"
:onclick="
(e, item) => {
changeActiveBtn(item);
const found = rtspDevices.find((r) => r.main_id === item.key);
if (found) selectDevice(found);
}
"
:className="`flex flex-wrap`"
size="sm"
color="info"
>
<template #buttonContent="{ item }">
<span class="text-base">{{ item.title }}</span>
</template>
</ButtonConnectedGroup>
</div>
<div class="flex h-[70%] gap-4">
<!-- 左側即時監控 -->
<div class="relative w-full flex-1 rounded border overflow-hidden">
<iframe
:src="monitorUrl"
class="absolute inset-0 w-full h-full"
allow="autoplay; fullscreen; picture-in-picture"
referrerpolicy="no-referrer"
></iframe>
</div>
<!-- 右側開始/結束偵測已移除選擇資料夾相關 UI -->
<aside class="w-1/2 flex flex-col gap-6 p-4">
<div class="flex gap-3">
<button
class="btn btn-add w-40"
@click="startDetection"
:disabled="isStarting || !selectedMainId"
:title="!selectedMainId ? $t('rtsp.pleaseSelectDevice') : ''"
>
{{ $t("rtsp.start") }}
</button>
<button
class="btn btn-error text-white w-40"
@click="stopDetection"
:disabled="isStopping || !selectedMainId"
:title="!selectedMainId ? $t('rtsp.pleaseSelectDevice') : ''"
>
{{ $t("rtsp.stop") }}
</button>
</div>
<p v-if="message" class="text-sm text-info">{{ message }}</p>
</aside>
</div>
</section>
</template>
<script>
import { getSystemDevices } from "@/apis/system";
import { setRtspEnable } from "@/apis/rtsp"; // setSambaDirectory
import useActiveBtn from "@/hooks/useActiveBtn";
const FILE_BASEURL = import.meta.env.VITE_FILE_API_BASEURL;
const DEFAULT_MONITOR_URL =
"http://192.168.0.219:8026/?url=rtsp://admin02:mjmAdmin_99@192.168.0.200:554/stream1?tcp";
export default {
name: "Rtsp",
setup() {
const { items, changeActiveBtn, setItems, selectedBtn } = useActiveBtn();
return { items, changeActiveBtn, setItems, selectedBtn };
},
data() {
return {
monitorUrl: DEFAULT_MONITOR_URL,
//
isStarting: false,
isStopping: false,
// UI
message: "",
//
deviceData: {},
rtspDevices: [], // { main_id, full_name, rtsp_url, ... }
selectedMainId: null, // main_id
};
},
async mounted() {
await this.getData();
},
watch: {
selectedBtn: {
handler(newVal) {
if (!newVal) return;
const found = this.rtspDevices.find((r) => r.main_id === newVal.key);
if (found) this.selectDevice(found);
},
deep: true,
},
},
methods: {
async getData() {
try {
const useBuildingStore = (await import("@/stores/useBuildingStore"))
.default;
const buildingStore = useBuildingStore();
const building_guid =
buildingStore?.selectedBuilding?.building_guid || "";
const res = await getSystemDevices({ building_guid });
const transformedData = {};
(res?.data || []).forEach((floor) => {
if (floor?.device_list?.length > 0) {
const fullUrl = floor.floor_map_name;
const uuid = fullUrl ? fullUrl.replace(/\.svg$/, "") : "";
transformedData[uuid] = floor.device_list.map((device) => {
let x = 0,
y = 0;
try {
const coordinates = JSON.parse(
device?.device_coordinate || "[0,0]"
);
x = Number(coordinates?.[0] ?? 0);
y = Number(coordinates?.[1] ?? 0);
} catch (_) {}
let state = "Online";
let bgColor = device?.device_normal_color;
if (
device?.device_status === "Offline" ||
device?.device_status == null
) {
state = "Offline";
bgColor = device?.device_close_color;
}
if (device?.device_status === "Error") {
state = "Error";
bgColor = device?.device_error_color;
}
return [
x,
y,
{
device_number: device?.device_number || "",
device_coordinate: device?.device_coordinate || "",
device_image_url: device?.device_image_url,
full_name: device?.full_name,
main_id: device?.main_id,
points: device?.points || [],
floor: floor?.full_name,
state,
icon: device?.device_image
? `${FILE_BASEURL}/upload/device_icon/${device.device_image}`
: "",
bgColor,
Online_color: device?.device_normal_color,
Offline_color: device?.device_close_color,
Error_color: device?.device_error_color,
brand: device?.brand || "",
device_model: device?.device_model,
operation_name: device?.operation_name,
operation_contact_person: device?.operation_contact_person,
buying_date: device?.buying_date,
created_at: device?.created_at,
bgSize: 50,
is_rtsp: device?.is_rtsp === true,
rtsp_url: device?.rtsp_url || "",
},
];
});
}
});
this.deviceData = transformedData;
const allRows = Object.values(transformedData).flat();
this.rtspDevices = allRows
.map((row) => row?.[2])
.filter((p) => p && p.is_rtsp && p.rtsp_url)
.reduce((acc, cur) => {
if (!acc.find((x) => x.main_id === cur.main_id)) acc.push(cur);
return acc;
}, [])
.sort((a, b) => (a.full_name || "").localeCompare(b.full_name || ""));
const cate = this.rtspDevices.map((d, index) => ({
title: d.full_name || d.main_id,
key: d.main_id,
active: this.selectedMainId
? this.selectedMainId === d.main_id
: index === 0,
...d,
}));
this.setItems(cate);
if (this.rtspDevices.length > 0) {
const first = this.rtspDevices[0];
this.selectedMainId = first.main_id;
this.monitorUrl = first.rtsp_url;
} else {
this.selectedMainId = null;
this.monitorUrl = DEFAULT_MONITOR_URL;
}
} catch (err) {
console.error("getData() 失敗", err);
this.setItems([]);
this.rtspDevices = [];
this.selectedMainId = null;
this.monitorUrl = DEFAULT_MONITOR_URL;
}
},
// Tab
selectDevice(d) {
this.selectedMainId = d.main_id;
this.monitorUrl = d.rtsp_url || DEFAULT_MONITOR_URL;
},
// setRtspEnable(true)
async startDetection() {
if (!this.selectedMainId) {
this.message = this.$t("rtsp.pleaseSelectDevice");
return;
}
this.isStarting = true;
try {
await setRtspEnable({
main_id: this.selectedMainId,
enable: true,
});
this.message = this.$t("rtsp.startSuccess");
} catch (e) {
console.error(e);
this.message = this.$t("rtsp.startFail");
} finally {
this.isStarting = false;
}
},
// setRtspEnable(false)
async stopDetection() {
if (!this.selectedMainId) {
this.message = this.$t("rtsp.pleaseSelectDevice");
return;
}
this.isStopping = true;
try {
await setRtspEnable({
main_id: this.selectedMainId,
enable: false,
});
this.message = this.$t("rtsp.stopSuccess");
} catch (e) {
console.error(e);
this.message = this.$t("rtsp.stopFail");
} finally {
this.isStopping = false;
}
},
},
};
</script>

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

@ -11,7 +11,7 @@ const { currentFloor, subscribeData } = 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,7 +8,7 @@ const { getCurrentInfoModalData, selected_dbid } = inject(
const { subscribeData } = 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;
const fitToView = (forge_dbid, spriteDbId) => {
selected_dbid.value = [forge_dbid, spriteDbId];

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([]);
@ -20,7 +20,7 @@ watch(
</script>
<template>
<!-- <a-carousel arrows class="mt-5 shadow-lg">
<a-carousel arrows class="mt-5 shadow-lg">
<template #prevArrow>
<div class="custom-slick-arrow" style="left: 10px; z-index: 1">
<font-awesome-icon
@ -42,7 +42,7 @@ watch(
<div v-for="(url, index) in imgData" :key="index">
<img :src="`${FILE_BASEURL}/${url}`" alt="Image" />
</div>
</a-carousel> -->
</a-carousel>
</template>
<style scoped>

File diff suppressed because one or more lines are too long

View File

@ -13,15 +13,6 @@ export default defineConfig({
outDir: "./dist",
emptyOutDir: true,
},
server: {
proxy: {
"/upload": {
target: "https://ibms-Empower.production.mjmtech.com.tw",
changeOrigin: true,
secure: false,
},
},
},
plugins: [
vue(),
Components({
@ -42,7 +33,6 @@ export default defineConfig({
},
},
resolve: {
dedupe: ["vue"],
alias: {
"@": fileURLToPath(new URL("./src", import.meta.url)),
"@ASSET": fileURLToPath(new URL("./src/assets", import.meta.url)),