Compare commits
4 Commits
feature/do
...
main
Author | SHA1 | Date | |
---|---|---|---|
29fd70a7fd | |||
759d353104 | |||
b7d4ef5a62 | |||
0ef5abe06d |
20
Docker/svc.front/.env
Normal file
20
Docker/svc.front/.env
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
# 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=新創賦能
|
21
Docker/svc.front/1.start.container.bat
Normal file
21
Docker/svc.front/1.start.container.bat
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
@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
|
27
Docker/svc.front/11.build-image.bat
Normal file
27
Docker/svc.front/11.build-image.bat
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
@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! ../../
|
47
Docker/svc.front/12.push-image.bat
Normal file
47
Docker/svc.front/12.push-image.bat
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
@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
|
22
Docker/svc.front/2.stop.container.bat
Normal file
22
Docker/svc.front/2.stop.container.bat
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
@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
|
||||||
|
|
22
Docker/svc.front/3.remove.container.bat
Normal file
22
Docker/svc.front/3.remove.container.bat
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
@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
|
||||||
|
|
22
Docker/svc.front/9.delete.all.bat
Normal file
22
Docker/svc.front/9.delete.all.bat
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
@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
|
||||||
|
|
43
Docker/svc.front/Dockerfile
Normal file
43
Docker/svc.front/Dockerfile
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
# 使用 Node.js 作為基礎映像
|
||||||
|
FROM node:18-alpine AS builder
|
||||||
|
|
||||||
|
# 設定工作目錄
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# 複製 package.json 和 package-lock.json (或 yarn.lock) 到工作目錄
|
||||||
|
COPY package*.json ./
|
||||||
|
|
||||||
|
# 安裝依賴
|
||||||
|
RUN npm install --legacy-peer-deps
|
||||||
|
|
||||||
|
# 複製所有檔案到工作目錄
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# 清理緩存並重新構建
|
||||||
|
RUN npm cache clean --force
|
||||||
|
RUN rm -rf node_modules
|
||||||
|
RUN npm install --legacy-peer-deps
|
||||||
|
|
||||||
|
# 構建前端應用 (如果需要)
|
||||||
|
RUN npm run build --omit=dev
|
||||||
|
|
||||||
|
# 使用一個更小的映像來提供靜態文件 (例如 Nginx)
|
||||||
|
FROM nginx:alpine
|
||||||
|
|
||||||
|
# 將構建好的靜態檔案複製到 Nginx 的預設目錄
|
||||||
|
COPY --from=builder /app/dist /usr/share/nginx/html
|
||||||
|
|
||||||
|
# (可選) 複製自定義 Nginx 設定檔
|
||||||
|
# COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||||
|
|
||||||
|
# 暴露 Nginx 預設的 80 端口
|
||||||
|
EXPOSE 80
|
||||||
|
|
||||||
|
# 2025-08-29 說明
|
||||||
|
LABEL changelog="2025-08-29: 打包測試。"
|
||||||
|
|
||||||
|
# Nginx 已經預設啟動,所以不需要 CMD 指令
|
||||||
|
COPY docker-entrypoint.sh /docker-entrypoint.sh
|
||||||
|
RUN chmod +x /docker-entrypoint.sh
|
||||||
|
ENTRYPOINT ["/docker-entrypoint.sh"]
|
||||||
|
CMD ["nginx", "-g", "daemon off;"]
|
31
Docker/svc.front/docker-compose.yml
Normal file
31
Docker/svc.front/docker-compose.yml
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
# 指令
|
||||||
|
# - 啓動容器: 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
|
@ -11,7 +11,7 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>新創賦能</title>
|
<title>新創賦能</title>
|
||||||
<script src="https://code.jquery.com/jquery-3.7.1.js"></script>
|
<script src="https://code.jquery.com/jquery-3.7.1.js"></script>
|
||||||
<script src="https://code.jquery.com/ui/1.13.3/jquery-ui.js"></script>
|
<!-- <script src="https://code.jquery.com/ui/1.13.3/jquery-ui.js"></script> -->
|
||||||
<!-- <script type="text/javascript" src="/requirejs/config.js"></script> -->
|
<!-- <script type="text/javascript" src="/requirejs/config.js"></script> -->
|
||||||
<!-- <script
|
<!-- <script
|
||||||
type="text/javascript"
|
type="text/javascript"
|
||||||
|
7
package-lock.json
generated
7
package-lock.json
generated
@ -21,6 +21,7 @@
|
|||||||
"dayjs": "^1.11.10",
|
"dayjs": "^1.11.10",
|
||||||
"echarts": "^5.4.3",
|
"echarts": "^5.4.3",
|
||||||
"flag-icons": "^7.2.3",
|
"flag-icons": "^7.2.3",
|
||||||
|
"hls.js": "^1.6.12",
|
||||||
"jquery-ui": "^1.14.1",
|
"jquery-ui": "^1.14.1",
|
||||||
"json-schema-generator": "^2.0.6",
|
"json-schema-generator": "^2.0.6",
|
||||||
"mqtt": "^5.10.3",
|
"mqtt": "^5.10.3",
|
||||||
@ -3056,6 +3057,12 @@
|
|||||||
"integrity": "sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==",
|
"integrity": "sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==",
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/htmlparser2": {
|
||||||
"version": "3.10.1",
|
"version": "3.10.1",
|
||||||
"resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.10.1.tgz",
|
"resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.10.1.tgz",
|
||||||
|
@ -22,6 +22,7 @@
|
|||||||
"dayjs": "^1.11.10",
|
"dayjs": "^1.11.10",
|
||||||
"echarts": "^5.4.3",
|
"echarts": "^5.4.3",
|
||||||
"flag-icons": "^7.2.3",
|
"flag-icons": "^7.2.3",
|
||||||
|
"hls.js": "^1.6.12",
|
||||||
"jquery-ui": "^1.14.1",
|
"jquery-ui": "^1.14.1",
|
||||||
"json-schema-generator": "^2.0.6",
|
"json-schema-generator": "^2.0.6",
|
||||||
"mqtt": "^5.10.3",
|
"mqtt": "^5.10.3",
|
||||||
|
5
src/apis/rtsp/api.js
Normal file
5
src/apis/rtsp/api.js
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
// 開關 RTSP(啟用/停用)
|
||||||
|
export const POST_SET_RTSP_ENABLE = `/api/rtsp/set-rtsp-enable`;
|
||||||
|
|
||||||
|
// 設定 SAMBA 儲存目錄
|
||||||
|
export const POST_SET_SAMBA_DIRECTORY = `/api/rtsp/set-samba-directory`;
|
29
src/apis/rtsp/index.js
Normal file
29
src/apis/rtsp/index.js
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
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 });
|
||||||
|
};
|
@ -106,7 +106,7 @@ watch(locale, () => {
|
|||||||
<img src="/logo.svg" alt="logo" class="w-6 lg:w-8 me-1" />
|
<img src="/logo.svg" alt="logo" class="w-6 lg:w-8 me-1" />
|
||||||
新創賦能
|
新創賦能
|
||||||
</router-link>
|
</router-link>
|
||||||
<NavbarBuilding class="hidden lg:block ms-8" />
|
<!-- <NavbarBuilding class="hidden lg:block ms-8" /> -->
|
||||||
</div>
|
</div>
|
||||||
<div class="hidden flex-1 lg:block">
|
<div class="hidden flex-1 lg:block">
|
||||||
<NavbarItem
|
<NavbarItem
|
||||||
|
@ -51,7 +51,7 @@
|
|||||||
"indoor_chart": "室內",
|
"indoor_chart": "室內",
|
||||||
"temperature": "温度",
|
"temperature": "温度",
|
||||||
"humidity": "湿度",
|
"humidity": "湿度",
|
||||||
"no_data":"无数据",
|
"no_data": "无数据",
|
||||||
"alerts_data": "异常资料"
|
"alerts_data": "异常资料"
|
||||||
},
|
},
|
||||||
"history": {
|
"history": {
|
||||||
@ -207,6 +207,9 @@
|
|||||||
"completed": "已完成",
|
"completed": "已完成",
|
||||||
"worker_id": "工作人员编号",
|
"worker_id": "工作人员编号",
|
||||||
"notice": "注意事项",
|
"notice": "注意事项",
|
||||||
|
"video_storage_location": "告警影片儲存位置",
|
||||||
|
"copy": "复制",
|
||||||
|
"copied": "已复制!",
|
||||||
"result_description": "结果描述",
|
"result_description": "结果描述",
|
||||||
"upload_file": "上传文件",
|
"upload_file": "上传文件",
|
||||||
"enable": "启用",
|
"enable": "启用",
|
||||||
@ -418,5 +421,21 @@
|
|||||||
"system_point_name": "系统点位名称",
|
"system_point_name": "系统点位名称",
|
||||||
"json_format_text": "请贴上 JSON 格式数据",
|
"json_format_text": "请贴上 JSON 格式数据",
|
||||||
"json_click_text": "请在左侧输入JSON并点选转换按钮"
|
"json_click_text": "请在左侧输入JSON并点选转换按钮"
|
||||||
|
},
|
||||||
|
"rtsp": {
|
||||||
|
"title": "影像串流",
|
||||||
|
"start": "开始侦测",
|
||||||
|
"stop": "结束侦测",
|
||||||
|
"selectPath": "选择存储位置",
|
||||||
|
"selectDevice": "选择设备",
|
||||||
|
"pleaseSelectDevice": "请先选择设备",
|
||||||
|
"selectPathFirst": "请先选择存储文件夹",
|
||||||
|
"startSuccess": "已开始侦测…",
|
||||||
|
"startFail": "开始侦测失败,请稍后再试",
|
||||||
|
"stopSuccess": "已请求结束侦测…",
|
||||||
|
"stopFail": "结束侦测失败,请稍后再试",
|
||||||
|
"noPermission": "未获得写入权限,请重新选择文件夹并授权",
|
||||||
|
"selectFolderSuccess": "已选择文件夹:{name}",
|
||||||
|
"selectFolderFail": "选择文件夹失败,请再试一次"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -207,6 +207,9 @@
|
|||||||
"completed": "已完成",
|
"completed": "已完成",
|
||||||
"worker_id": "工作人員編號",
|
"worker_id": "工作人員編號",
|
||||||
"notice": "注意事項",
|
"notice": "注意事項",
|
||||||
|
"video_storage_location": "告警影片儲存位置",
|
||||||
|
"copy": "複製",
|
||||||
|
"copied": "已複製!",
|
||||||
"result_description": "結果描述",
|
"result_description": "結果描述",
|
||||||
"upload_file": "上傳檔案",
|
"upload_file": "上傳檔案",
|
||||||
"enable": "啟用",
|
"enable": "啟用",
|
||||||
@ -418,5 +421,21 @@
|
|||||||
"system_point_name": "系統點位名稱",
|
"system_point_name": "系統點位名稱",
|
||||||
"json_format_text": "請貼上 JSON 格式數據",
|
"json_format_text": "請貼上 JSON 格式數據",
|
||||||
"json_click_text": "請在左側輸入JSON並點選轉換按鈕"
|
"json_click_text": "請在左側輸入JSON並點選轉換按鈕"
|
||||||
|
},
|
||||||
|
"rtsp": {
|
||||||
|
"title": "影像串流",
|
||||||
|
"start": "開始偵測",
|
||||||
|
"stop": "結束偵測",
|
||||||
|
"selectPath": "選擇儲存位置",
|
||||||
|
"selectDevice": "選擇設備",
|
||||||
|
"pleaseSelectDevice": "請先選擇設備",
|
||||||
|
"selectPathFirst": "請先選擇儲存資料夾",
|
||||||
|
"startSuccess": "已開始偵測…",
|
||||||
|
"startFail": "開始偵測失敗,請稍後再試",
|
||||||
|
"stopSuccess": "已請求結束偵測…",
|
||||||
|
"stopFail": "結束偵測失敗,請稍後再試",
|
||||||
|
"noPermission": "沒有取得寫入權限,請重新選擇資料夾並允許",
|
||||||
|
"selectFolderSuccess": "已選擇資料夾:{name}",
|
||||||
|
"selectFolderFail": "選擇資料夾失敗,請再試一次"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -51,7 +51,7 @@
|
|||||||
"indoor_chart": "Indoor",
|
"indoor_chart": "Indoor",
|
||||||
"temperature": "Temp.",
|
"temperature": "Temp.",
|
||||||
"humidity": "Hum.",
|
"humidity": "Hum.",
|
||||||
"no_data":"No data",
|
"no_data": "No data",
|
||||||
"alerts_data": "Abnormal data"
|
"alerts_data": "Abnormal data"
|
||||||
},
|
},
|
||||||
"history": {
|
"history": {
|
||||||
@ -207,6 +207,9 @@
|
|||||||
"completed": "Completed",
|
"completed": "Completed",
|
||||||
"worker_id": "Worker ID",
|
"worker_id": "Worker ID",
|
||||||
"notice": "Notice",
|
"notice": "Notice",
|
||||||
|
"video_storage_location": "video storage location",
|
||||||
|
"copy": "Copy",
|
||||||
|
"copied": "Copied!",
|
||||||
"result_description": "Result Description",
|
"result_description": "Result Description",
|
||||||
"upload_file": "Upload File",
|
"upload_file": "Upload File",
|
||||||
"enable": "Enable",
|
"enable": "Enable",
|
||||||
@ -418,5 +421,21 @@
|
|||||||
"system_point_name": "System Point Name",
|
"system_point_name": "System Point Name",
|
||||||
"json_format_text": "Please paste JSON format data",
|
"json_format_text": "Please paste JSON format data",
|
||||||
"json_click_text": "Please enter JSON on the left and click the conversion button"
|
"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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -67,4 +67,10 @@ export const AUTHPAGES = [
|
|||||||
pageName: "Setting",
|
pageName: "Setting",
|
||||||
navigate: "/Setting",
|
navigate: "/Setting",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
authCode: "PF12",
|
||||||
|
icon: "camera",
|
||||||
|
pageName: "rtsp",
|
||||||
|
navigate: "/rtsp",
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
@ -63,7 +63,8 @@ import {
|
|||||||
faSave,
|
faSave,
|
||||||
faCrown,
|
faCrown,
|
||||||
faClock,
|
faClock,
|
||||||
faCheckCircle
|
faCheckCircle,
|
||||||
|
faCamera
|
||||||
} from "@fortawesome/free-solid-svg-icons";
|
} from "@fortawesome/free-solid-svg-icons";
|
||||||
import { faCircle } from "@fortawesome/free-regular-svg-icons";
|
import { faCircle } from "@fortawesome/free-regular-svg-icons";
|
||||||
|
|
||||||
@ -130,6 +131,7 @@ library.add(
|
|||||||
faCrown,
|
faCrown,
|
||||||
faClock,
|
faClock,
|
||||||
faCheckCircle,
|
faCheckCircle,
|
||||||
|
faCamera,
|
||||||
faCircle
|
faCircle
|
||||||
);
|
);
|
||||||
|
|
||||||
|
73
src/main.js
73
src/main.js
@ -1,60 +1,83 @@
|
|||||||
|
// ---- styles ----
|
||||||
import "./assets/index.css";
|
import "./assets/index.css";
|
||||||
import "./assets/main.css";
|
import "./assets/main.css";
|
||||||
// import "./assets/table.css";
|
|
||||||
import "./assets/btn.css";
|
import "./assets/btn.css";
|
||||||
import "./assets/pagination.css";
|
import "./assets/pagination.css";
|
||||||
|
|
||||||
import { createApp } from "vue";
|
// ---- Vue core ----
|
||||||
|
import { createApp, onErrorCaptured } from "vue";
|
||||||
import { createI18n } from "vue-i18n";
|
import { createI18n } from "vue-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 { createPinia } from "pinia";
|
||||||
|
|
||||||
|
// ---- App / Router ----
|
||||||
import App from "./App.vue";
|
import App from "./App.vue";
|
||||||
import router from "./router";
|
import router from "./router";
|
||||||
|
|
||||||
|
// ---- UI / Icons / Global comps ----
|
||||||
|
import Antd from "ant-design-vue";
|
||||||
import "virtual:svg-icons-register";
|
import "virtual:svg-icons-register";
|
||||||
// 引入项目中的全部全局组件
|
import SvgIcon from "@/components/SvgIcon.vue";
|
||||||
import SvgIcon from "@/components/svgIcon.vue";
|
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
|
||||||
import library from "./fontawsomeIconRegister";
|
import library from "./fontawsomeIconRegister";
|
||||||
import "flag-icons/css/flag-icons.min.css";
|
import "flag-icons/css/flag-icons.min.css";
|
||||||
|
|
||||||
/* import font awesome icon component */
|
// ---- Directives ----
|
||||||
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
|
|
||||||
|
|
||||||
import { focusPlugin } from "@/directives/focusPlugin";
|
import { focusPlugin } from "@/directives/focusPlugin";
|
||||||
import { draggable } from "@/directives/draggable";
|
import { draggable } from "@/directives/draggable";
|
||||||
const messages = {
|
|
||||||
tw,
|
|
||||||
cn,
|
|
||||||
us,
|
|
||||||
};
|
|
||||||
|
|
||||||
|
// ---- i18n ----
|
||||||
|
import tw from "./config/tw.json";
|
||||||
|
import cn from "./config/cn.json";
|
||||||
|
import us from "./config/us.json";
|
||||||
|
|
||||||
|
const messages = { tw, cn, us };
|
||||||
const storedLanguage = localStorage.getItem("EmpowerLanguage") || "tw";
|
const storedLanguage = localStorage.getItem("EmpowerLanguage") || "tw";
|
||||||
|
|
||||||
const i18n = createI18n({
|
const i18n = createI18n({
|
||||||
legacy: false,
|
legacy: false,
|
||||||
locale: storedLanguage,
|
locale: storedLanguage,
|
||||||
fallbackLocale: 'tw',
|
fallbackLocale: "tw",
|
||||||
messages,
|
messages,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 建立 App(確保只 mount 一次)
|
||||||
|
// ===========================================
|
||||||
const app = createApp(App);
|
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(createPinia());
|
||||||
app.use(router);
|
app.use(router);
|
||||||
app.use(Antd);
|
app.use(Antd);
|
||||||
app.use(i18n);
|
app.use(i18n);
|
||||||
|
|
||||||
// 组装成一个对象
|
// 全域元件註冊(維持你的寫法)
|
||||||
const allGlobalComponents = { SvgIcon, FontAwesomeIcon };
|
const allGlobalComponents = { SvgIcon, FontAwesomeIcon };
|
||||||
const globalComponent = {
|
app.use({
|
||||||
install(app) {
|
install(app) {
|
||||||
// 循环注册所有的全局组件
|
Object.keys(allGlobalComponents).forEach((k) => {
|
||||||
Object.keys(allGlobalComponents).forEach((componentName) => {
|
app.component(k, allGlobalComponents[k]);
|
||||||
app.component(componentName, allGlobalComponents[componentName]);
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
};
|
});
|
||||||
app.use(globalComponent);
|
|
||||||
|
// 指令
|
||||||
app.use(focusPlugin);
|
app.use(focusPlugin);
|
||||||
app.use(draggable);
|
app.use(draggable);
|
||||||
|
|
||||||
|
// ===========================================
|
||||||
|
// Mount(保證只呼叫一次)
|
||||||
|
// ===========================================
|
||||||
app.mount("#app");
|
app.mount("#app");
|
||||||
|
@ -56,6 +56,12 @@ const router = createRouter({
|
|||||||
name: "assetManagement",
|
name: "assetManagement",
|
||||||
component: () => import("@/views/AssetManagement/AssetManagement.vue"),
|
component: () => import("@/views/AssetManagement/AssetManagement.vue"),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "/rtsp",
|
||||||
|
name: "rtsp",
|
||||||
|
component: () => import("@/views/rtsp/Rtsp.vue"),
|
||||||
|
meta: { layout: "map", title: "rtsp" },
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: "/alert",
|
path: "/alert",
|
||||||
name: "alert",
|
name: "alert",
|
||||||
|
@ -91,12 +91,12 @@ watch(selectedBtn, (newValue) => {
|
|||||||
:getData="getMainSystems"
|
:getData="getMainSystems"
|
||||||
:formState="formState"
|
:formState="formState"
|
||||||
/>
|
/>
|
||||||
<!-- <button
|
<button
|
||||||
@click.stop.prevent="isEditMode = !isEditMode"
|
@click.stop.prevent="isEditMode = !isEditMode"
|
||||||
class="btn btn-sm btn-outline-success"
|
class="btn btn-sm btn-outline-success"
|
||||||
>
|
>
|
||||||
{{ isEditMode ? t("button.stop_edit") : t("button.start_edit") }}
|
{{ isEditMode ? t("button.stop_edit") : t("button.start_edit") }}
|
||||||
</button> -->
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<ButtonConnectedGroup
|
<ButtonConnectedGroup
|
||||||
:items="items"
|
:items="items"
|
||||||
|
@ -51,9 +51,9 @@ const onReset = () => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<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") }}
|
<font-awesome-icon :icon="['fas', 'plus']" />{{ $t("button.add") }}
|
||||||
</button> -->
|
</button>
|
||||||
<Modal
|
<Modal
|
||||||
id="asset_add_main_item"
|
id="asset_add_main_item"
|
||||||
:title="
|
:title="
|
||||||
|
@ -155,7 +155,7 @@ watch(
|
|||||||
<template #topLeft>IoT</template>
|
<template #topLeft>IoT</template>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</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
|
<Select
|
||||||
:value="formState"
|
:value="formState"
|
||||||
class="min-w-[180px] w-full"
|
class="min-w-[180px] w-full"
|
||||||
@ -169,7 +169,7 @@ watch(
|
|||||||
$t("energy.electricity_classification")
|
$t("energy.electricity_classification")
|
||||||
}}</template>
|
}}</template>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div> -->
|
||||||
<Input :value="formState" class="min-w-[180px] w-full" name="asset_number">
|
<Input :value="formState" class="min-w-[180px] w-full" name="asset_number">
|
||||||
<template #topLeft>{{ $t("assetManagement.asset_number") }}</template>
|
<template #topLeft>{{ $t("assetManagement.asset_number") }}</template>
|
||||||
<template #bottomLeft
|
<template #bottomLeft
|
||||||
|
@ -1,5 +1,13 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, defineProps, watch, inject } from "vue";
|
import {
|
||||||
|
ref,
|
||||||
|
defineProps,
|
||||||
|
watch,
|
||||||
|
inject,
|
||||||
|
nextTick,
|
||||||
|
onMounted,
|
||||||
|
toRaw,
|
||||||
|
} from "vue";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import { postOperationRecord } from "@/apis/alert";
|
import { postOperationRecord } from "@/apis/alert";
|
||||||
import * as yup from "yup";
|
import * as yup from "yup";
|
||||||
@ -13,6 +21,27 @@ const props = defineProps({
|
|||||||
editRecord: Object,
|
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 form = ref(null);
|
||||||
|
|
||||||
const dateItem = ref([
|
const dateItem = ref([
|
||||||
@ -64,18 +93,43 @@ const updateFileList = (files) => {
|
|||||||
formState.value.lorf = 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 onOk = async () => {
|
||||||
const formData = new FormData(form.value);
|
const formData = new FormData(form.value);
|
||||||
formData.delete("oriFile");
|
formData.delete("oriFile");
|
||||||
|
|
||||||
formState.value?.lorf.forEach((file, index) => {
|
(formState.value?.lorf ?? []).forEach((file, index) => {
|
||||||
formData.append(`lorf[${index}].id`, file.id ? file.id : "");
|
formData.append(`lorf[${index}].id`, file?.id ? file.id : "");
|
||||||
formData.append(`lorf[${index}].file`, file.id ? null : file);
|
// 只有新檔案才上傳檔案本體
|
||||||
|
if (!file?.id && file) {
|
||||||
|
formData.append(`lorf[${index}].file`, file);
|
||||||
|
}
|
||||||
formData.append(
|
formData.append(
|
||||||
`lorf[${index}].save_file_name`,
|
`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(
|
formData.append(
|
||||||
@ -83,9 +137,9 @@ const onOk = async () => {
|
|||||||
dayjs(dateItem.value[0].value).format("YYYY-MM-DD")
|
dayjs(dateItem.value[0].value).format("YYYY-MM-DD")
|
||||||
);
|
);
|
||||||
|
|
||||||
props.editRecord.id && formData.append("id", props.editRecord.id);
|
if (props.editRecord.id) formData.append("id", props.editRecord.id);
|
||||||
|
if (props.editRecord.uuid)
|
||||||
props.editRecord.uuid && formData.append("error_code", props.editRecord.uuid);
|
formData.append("error_code", props.editRecord.uuid);
|
||||||
|
|
||||||
formData.append("work_type", 2);
|
formData.append("work_type", 2);
|
||||||
formData.append(
|
formData.append(
|
||||||
@ -96,7 +150,7 @@ const onOk = async () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const value = await handleSubmit(alertSchema, formState.value);
|
await handleSubmit(alertSchema, formState.value);
|
||||||
const res = await postOperationRecord(formData);
|
const res = await postOperationRecord(formData);
|
||||||
if (res.isSuccess) {
|
if (res.isSuccess) {
|
||||||
search?.();
|
search?.();
|
||||||
@ -123,32 +177,36 @@ const onCancel = () => {
|
|||||||
description: "",
|
description: "",
|
||||||
lorf: [],
|
lorf: [],
|
||||||
};
|
};
|
||||||
|
// 重置顯示用的影片路徑
|
||||||
|
videoLocation.value.videoLocation = "";
|
||||||
handleErrorReset();
|
handleErrorReset();
|
||||||
updateEditRecord?.(null);
|
updateEditRecord?.(null);
|
||||||
alert_action_item.close();
|
alert_action_item.close();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 同步 props.editRecord -> formState / 日期 / 維修項目 / 影片網址
|
||||||
watch(
|
watch(
|
||||||
() => props.editRecord,
|
() => props.editRecord,
|
||||||
(newVal) => {
|
(newVal) => {
|
||||||
|
debugLog("watch props.editRecord changed", newVal);
|
||||||
if (newVal) {
|
if (newVal) {
|
||||||
for (let [key, value] of Object.entries(newVal)) {
|
for (let [key, value] of Object.entries(newVal)) {
|
||||||
// 僅更新 formState 中已有的屬性
|
|
||||||
if (key in formState.value) {
|
if (key in formState.value) {
|
||||||
formState.value[key] = value;
|
formState.value[key] = value;
|
||||||
}
|
}
|
||||||
// 處理 start_time
|
|
||||||
if (key === "start_time") {
|
if (key === "start_time") {
|
||||||
formState.value.start_time = value
|
const d = value ? dayjs(value) : dayjs();
|
||||||
? dayjs(value).format("YYYY-MM-DD")
|
formState.value.start_time = d.format("YYYY-MM-DD");
|
||||||
: dayjs().format("YYYY-MM-DD");
|
dateItem.value[0].value = d; // 一律用 dayjs 物件
|
||||||
dateItem.value[0].value = value;
|
|
||||||
}
|
}
|
||||||
// 維修項目
|
|
||||||
if (key === "full_name") {
|
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 }
|
{ immediate: true }
|
||||||
@ -163,7 +221,11 @@ watch(
|
|||||||
width="710"
|
width="710"
|
||||||
>
|
>
|
||||||
<template #modalContent>
|
<template #modalContent>
|
||||||
<form ref="form" class="mt-5 w-full flex flex-wrap justify-between">
|
<form
|
||||||
|
ref="form"
|
||||||
|
class="mt-5 w-full flex flex-wrap justify-between"
|
||||||
|
@submit.prevent
|
||||||
|
>
|
||||||
<Input
|
<Input
|
||||||
v-if="formState && formState.formId"
|
v-if="formState && formState.formId"
|
||||||
class="my-2"
|
class="my-2"
|
||||||
@ -286,9 +348,48 @@ watch(
|
|||||||
</span></template
|
</span></template
|
||||||
>
|
>
|
||||||
</Select>
|
</Select>
|
||||||
|
|
||||||
|
<!-- 注意事項 -->
|
||||||
<Textarea :value="formState" name="notice" class="w-full my-2">
|
<Textarea :value="formState" name="notice" class="w-full my-2">
|
||||||
<template #topLeft>{{ $t("alert.notice") }}</template>
|
<template #topLeft>{{ $t("alert.notice") }}</template>
|
||||||
</Textarea>
|
</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">
|
<Textarea :value="formState" name="description" class="w-full my-2">
|
||||||
<template #topLeft>{{ $t("alert.result_description") }}</template>
|
<template #topLeft>{{ $t("alert.result_description") }}</template>
|
||||||
</Textarea>
|
</Textarea>
|
||||||
|
@ -100,6 +100,8 @@ const getData = async () => {
|
|||||||
buying_date: device.buying_date,
|
buying_date: device.buying_date,
|
||||||
created_at: device.created_at,
|
created_at: device.created_at,
|
||||||
bgSize: 50,
|
bgSize: 50,
|
||||||
|
is_rtsp: device.is_rtsp === true,
|
||||||
|
rtsp_url: device.rtsp_url || "",
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
});
|
});
|
||||||
@ -134,20 +136,14 @@ onUnmounted(() => {
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex flex-wrap justify-between">
|
<div class="flex flex-wrap justify-between">
|
||||||
<div
|
<div
|
||||||
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"
|
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"
|
||||||
>
|
>
|
||||||
<!-- 無資料時:完整隱藏區塊,不留空白 -->
|
<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 />
|
<DashboardIndoor />
|
||||||
<DashboardRefrig class="mb-10" />
|
</div>
|
||||||
|
<div class="flex flex-col gap-5">
|
||||||
|
<DashboardRefrig />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
@ -156,19 +152,19 @@ onUnmounted(() => {
|
|||||||
<DashboardFloorBar />
|
<DashboardFloorBar />
|
||||||
<DashboardEffectScatter :data="systemData" />
|
<DashboardEffectScatter :data="systemData" />
|
||||||
</div>
|
</div>
|
||||||
<div class="order-2 w-full lg:hidden my-3">
|
<!-- <div class="order-2 w-full lg:hidden my-3">
|
||||||
<DashboardSysCard :data="systemData" />
|
<DashboardSysCard :data="systemData" />
|
||||||
</div>
|
</div> -->
|
||||||
<div
|
<div
|
||||||
class="order-last w-full lg:w-1/4 flex flex-col justify-start border-dashboard z-20 gap-5"
|
class="order-last w-full lg:w-1/4 flex flex-col justify-start border-dashboard z-20 gap-12"
|
||||||
>
|
>
|
||||||
<div>
|
<div class="flex flex-col gap-5">
|
||||||
<DashboardElectricity />
|
<DashboardElectricity />
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-10">
|
<div class="flex flex-col gap-5">
|
||||||
<DashboardEmission />
|
<DashboardEmission />
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-10">
|
<div class="flex flex-col gap-5">
|
||||||
<DashboardAlert />
|
<DashboardAlert />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -4,6 +4,7 @@ import EffectScatter from "@/components/chart/EffectScatter.vue";
|
|||||||
import DashboardEffectScatterModal from "./DashboardEffectScatterModal.vue";
|
import DashboardEffectScatterModal from "./DashboardEffectScatterModal.vue";
|
||||||
import useSearchParam from "@/hooks/useSearchParam";
|
import useSearchParam from "@/hooks/useSearchParam";
|
||||||
import { computed, inject, ref, watch } from "vue";
|
import { computed, inject, ref, watch } from "vue";
|
||||||
|
import { twMerge } from "tailwind-merge";
|
||||||
import { useI18n } from "vue-i18n";
|
import { useI18n } from "vue-i18n";
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
@ -91,46 +92,48 @@ const handleItemClick = (params) => {
|
|||||||
watch(
|
watch(
|
||||||
[searchParams, () => asset_floor_chart.value, () => props.data],
|
[searchParams, () => asset_floor_chart.value, () => props.data],
|
||||||
([newValue, newChart, newData], [oldValue]) => {
|
([newValue, newChart, newData], [oldValue]) => {
|
||||||
console.groupCollapsed("[FloorMap] watch fired");
|
if (
|
||||||
console.log("floor_id =", newValue.floor_id);
|
newValue.floor_id &&
|
||||||
console.log("chart ready =", !!newChart);
|
newChart &&
|
||||||
console.log("data keys =", Object.keys(newData || {}).length);
|
Object.keys(newData || {}).length > 0
|
||||||
console.groupEnd();
|
) {
|
||||||
const floorId = newValue.floor_id;
|
const isFloorChanged = currentFloorId.value !== newValue.floor_id;
|
||||||
const chartReady = !!newChart;
|
|
||||||
// (1) 首次/樓層切換:不等 data,先載 SVG
|
if (isFloorChanged) {
|
||||||
if (floorId && chartReady && currentFloorId.value !== floorId) {
|
// 樓層切換時才重新載入 SVG
|
||||||
console.log("[FloorMap] load SVG for floor:", floorId);
|
console.log(
|
||||||
currentFloorId.value = floorId;
|
"Floor changed, updating chart with new SVG",
|
||||||
|
newValue.floor_id
|
||||||
|
);
|
||||||
|
currentFloorId.value = newValue.floor_id;
|
||||||
newChart.updateSvg(
|
newChart.updateSvg(
|
||||||
{
|
{
|
||||||
full_name: floorId,
|
full_name: newValue.floor_id,
|
||||||
path: `${FILE_BASEURL}/upload/floor_map/${floorId}.svg`,
|
path: `${FILE_BASEURL}/upload/floor_map/${newValue.floor_id}.svg`,
|
||||||
},
|
},
|
||||||
defaultOption(floorId, []) // 先不帶點位
|
defaultOption(newValue.floor_id, currentIconData.value)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// 添加點擊事件監聽
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (newChart.chart) {
|
if (newChart.chart) {
|
||||||
newChart.chart.off("click");
|
newChart.chart.off("click"); // 移除舊的監聽器
|
||||||
newChart.chart.on("click", handleItemClick);
|
newChart.chart.on("click", handleItemClick);
|
||||||
}
|
}
|
||||||
}, 100);
|
}, 100);
|
||||||
}
|
} else if (currentFloorId.value === newValue.floor_id && newChart.chart) {
|
||||||
|
// 只是資料更新時,只更新圖表資料,不重新載入 SVG
|
||||||
// (2) 資料到齊後,再補點位(不重載 SVG)
|
console.log("Data updated, refreshing chart data only");
|
||||||
if (
|
|
||||||
floorId &&
|
|
||||||
chartReady &&
|
|
||||||
Object.keys(newData || {}).length > 0 &&
|
|
||||||
newChart.chart
|
|
||||||
) {
|
|
||||||
console.log("[FloorMap] update series only for floor:", floorId);
|
|
||||||
newChart.chart.setOption(
|
newChart.chart.setOption(
|
||||||
{ series: defaultOption(floorId, currentIconData.value).series },
|
{
|
||||||
|
series: defaultOption(newValue.floor_id, currentIconData.value)
|
||||||
|
.series,
|
||||||
|
},
|
||||||
false,
|
false,
|
||||||
true
|
true
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
immediate: true,
|
immediate: true,
|
||||||
|
@ -6,10 +6,7 @@ import { useI18n } from "vue-i18n";
|
|||||||
import { twMerge } from "tailwind-merge";
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const currentTab = ref("desktop");
|
|
||||||
const changeOpenKey = (key) => {
|
|
||||||
currentTab.value = key;
|
|
||||||
};
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
data: {
|
data: {
|
||||||
type: Object,
|
type: Object,
|
||||||
@ -17,6 +14,16 @@ 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);
|
const modal = ref(null);
|
||||||
|
|
||||||
// 當 data 有值時自動開啟 modal
|
// 當 data 有值時自動開啟 modal
|
||||||
@ -24,15 +31,23 @@ watch(
|
|||||||
() => props.data,
|
() => props.data,
|
||||||
(newData) => {
|
(newData) => {
|
||||||
if (newData) {
|
if (newData) {
|
||||||
|
console.log("[props.data] =\n", JSON.stringify(props.data, null, 2));
|
||||||
dashboard_effectScatter_modal.showModal();
|
dashboard_effectScatter_modal.showModal();
|
||||||
|
if (!isRtsp.value) currentTab.value = "desktop";
|
||||||
|
console.debug(
|
||||||
|
"[Modal Debug] is_rtsp:",
|
||||||
|
newData.is_rtsp,
|
||||||
|
"monitorUrl:",
|
||||||
|
monitorUrl.value
|
||||||
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{ immediate: true }
|
{ immediate: true }
|
||||||
);
|
);
|
||||||
|
|
||||||
// 關閉 modal 的處理函數
|
// 關閉 modal
|
||||||
const handleCancel = () => {
|
const handleCancel = () => {
|
||||||
currentTab.value = "desktop"; // 重置到 desktop tab
|
currentTab.value = "desktop";
|
||||||
dashboard_effectScatter_modal.close();
|
dashboard_effectScatter_modal.close();
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
@ -47,10 +62,13 @@ const handleCancel = () => {
|
|||||||
:draggable="true"
|
:draggable="true"
|
||||||
modalClass="max-h-[80vh]"
|
modalClass="max-h-[80vh]"
|
||||||
>
|
>
|
||||||
|
<!-- 標題列:RTSP 不顯示分頁鈕;非 RTSP 顯示分頁鈕 -->
|
||||||
<template #modalTitle>
|
<template #modalTitle>
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<span>{{ props.data?.full_name }}</span>
|
<span>{{ props.data?.full_name }}</span>
|
||||||
|
|
||||||
<div class="flex items-center space-x-2">
|
<div class="flex items-center space-x-2">
|
||||||
|
<template v-if="!isRtsp">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="text-base btn-link btn-text-without-border px-2"
|
class="text-base btn-link btn-text-without-border px-2"
|
||||||
@ -96,7 +114,7 @@ const handleCancel = () => {
|
|||||||
"
|
"
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
<Button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="text-base btn-link btn-text-without-border px-2"
|
class="text-base btn-link btn-text-without-border px-2"
|
||||||
@click="() => changeOpenKey('chart')"
|
@click="() => changeOpenKey('chart')"
|
||||||
@ -110,7 +128,9 @@ const handleCancel = () => {
|
|||||||
)
|
)
|
||||||
"
|
"
|
||||||
/>
|
/>
|
||||||
</Button>
|
</button>
|
||||||
|
</template>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="btn-link btn-text-without-border px-2"
|
class="btn-link btn-text-without-border px-2"
|
||||||
@ -126,7 +146,20 @@ const handleCancel = () => {
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #modalContent>
|
<template #modalContent>
|
||||||
<div class="space-y-4 py-4">
|
<!-- 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">
|
||||||
<!-- Desktop Tab - 基本資訊 -->
|
<!-- Desktop Tab - 基本資訊 -->
|
||||||
<div v-if="currentTab === 'desktop'" class="grid grid-cols-1 gap-4">
|
<div v-if="currentTab === 'desktop'" class="grid grid-cols-1 gap-4">
|
||||||
<table
|
<table
|
||||||
@ -174,48 +207,6 @@ const handleCancel = () => {
|
|||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</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>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
@ -257,24 +248,35 @@ const handleCancel = () => {
|
|||||||
</td>
|
</td>
|
||||||
<td class="p-2 border">{{ props.data.device_coordinate }}</td>
|
<td class="p-2 border">{{ props.data.device_coordinate }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
<tr>
|
<tr>
|
||||||
<td class="p-2 border">{{ $t("assetManagement.brand_and_modal") }}</td>
|
<td class="p-2 border">
|
||||||
<td class="p-2 border">{{ props.data.brand }} / {{ props.data.device_model }}</td>
|
{{ $t("assetManagement.brand_and_modal") }}
|
||||||
|
</td>
|
||||||
|
<td class="p-2 border">
|
||||||
|
{{ props.data.brand }} / {{ props.data.device_model }}
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td class="p-2 border">{{ $t("assetManagement.company_and_contact") }}</td>
|
<td class="p-2 border">
|
||||||
<td class="p-2 border">{{ props.data.operation_name }} / {{ props.data.operation_contact_person }}</td>
|
{{ $t("assetManagement.company_and_contact") }}
|
||||||
|
</td>
|
||||||
|
<td class="p-2 border">
|
||||||
|
{{ props.data.operation_name }} /
|
||||||
|
{{ props.data.operation_contact_person }}
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<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>
|
<td class="p-2 border">{{ props.data.buying_date }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<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>
|
<td class="p-2 border">{{ props.data.created_at }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
@ -284,5 +286,9 @@ const handleCancel = () => {
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
/* 可以添加額外的樣式 */
|
/* 讓 Modal 內容能撐滿高度 */
|
||||||
|
:deep(.min-h-\[200px\]) {
|
||||||
|
min-height: 0 !important;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -8,6 +8,7 @@ import useBuildingStore from "@/stores/useBuildingStore";
|
|||||||
const store = useBuildingStore();
|
const store = useBuildingStore();
|
||||||
const { t, locale } = useI18n();
|
const { t, locale } = useI18n();
|
||||||
const taipower_data = ref([]);
|
const taipower_data = ref([]);
|
||||||
|
const elecUseDayData = ref([]);
|
||||||
const carbonValue = ref(null);
|
const carbonValue = ref(null);
|
||||||
const carbonData = ref(null);
|
const carbonData = ref(null);
|
||||||
const search_data = computed(() => {
|
const search_data = computed(() => {
|
||||||
@ -107,6 +108,7 @@ watch(
|
|||||||
JSON.stringify(newValue) !== JSON.stringify(oldValue)
|
JSON.stringify(newValue) !== JSON.stringify(oldValue)
|
||||||
) {
|
) {
|
||||||
getData(newValue);
|
getData(newValue);
|
||||||
|
getElecUseDayData(newValue);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -50,10 +50,11 @@ const defaultChartOption = ref({
|
|||||||
tooltip: { trigger: "axis" },
|
tooltip: { trigger: "axis" },
|
||||||
legend: {
|
legend: {
|
||||||
data: [],
|
data: [],
|
||||||
textStyle: { color: "#ffffff", fontSize: 16 },
|
top: 0, // 靠最上方
|
||||||
|
textStyle: { color: "#ffffff", fontSize: 12 },
|
||||||
},
|
},
|
||||||
grid: {
|
grid: {
|
||||||
top: "10%",
|
top: "35%",
|
||||||
left: "0%",
|
left: "0%",
|
||||||
right: "0%",
|
right: "0%",
|
||||||
bottom: "0%",
|
bottom: "0%",
|
||||||
@ -203,7 +204,7 @@ onUnmounted(() => {
|
|||||||
<h3 class="text-info text-xl text-center">
|
<h3 class="text-info text-xl text-center">
|
||||||
{{ $t("dashboard.indoor_chart") }}
|
{{ $t("dashboard.indoor_chart") }}
|
||||||
</h3>
|
</h3>
|
||||||
<div className="my-3 w-full flex justify-center relative">
|
<div class="w-full flex justify-center items-center relative">
|
||||||
<ButtonConnectedGroup
|
<ButtonConnectedGroup
|
||||||
:items="items"
|
:items="items"
|
||||||
:onclick="(e, item) => changeActiveBtn(item)"
|
:onclick="(e, item) => changeActiveBtn(item)"
|
||||||
|
@ -1,150 +1,61 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import LineChart from "@/components/chart/LineChart.vue";
|
import LineChart from "@/components/chart/LineChart.vue";
|
||||||
import { SECOND_CHART_COLOR } from "@/constant";
|
import { SECOND_CHART_COLOR } from "@/constant";
|
||||||
import { ref, watch, computed, onUnmounted } from "vue";
|
import dayjs from "dayjs";
|
||||||
|
import { ref, watch, onUnmounted, computed } from "vue";
|
||||||
import useActiveBtn from "@/hooks/useActiveBtn";
|
import useActiveBtn from "@/hooks/useActiveBtn";
|
||||||
import { getDashboardTemp } from "@/apis/dashboard";
|
import { getDashboardTemp } from "@/apis/dashboard";
|
||||||
import useSearchParams from "@/hooks/useSearchParam";
|
import useSearchParams from "@/hooks/useSearchParam";
|
||||||
import useBuildingStore from "@/stores/useBuildingStore";
|
import useBuildingStore from "@/stores/useBuildingStore";
|
||||||
import { useI18n } from "vue-i18n";
|
import { useI18n } from "vue-i18n";
|
||||||
import dayjs from "dayjs";
|
|
||||||
|
|
||||||
const { t, locale } = useI18n();
|
const { t, locale } = useI18n();
|
||||||
const { searchParams } = useSearchParams();
|
const { searchParams } = useSearchParams();
|
||||||
const buildingStore = useBuildingStore();
|
const buildingStore = useBuildingStore();
|
||||||
const allTempData = ref([]);
|
const timeoutTimer = ref(null); // 定時器
|
||||||
|
|
||||||
// 狀態與按鈕邏輯
|
|
||||||
const timeoutTimer = ref(null);
|
|
||||||
const { items, changeActiveBtn, setItems, selectedBtn } = useActiveBtn();
|
const { items, changeActiveBtn, setItems, selectedBtn } = useActiveBtn();
|
||||||
const currentOptionType = ref(1); // 1: 溫度, 2: 濕度
|
|
||||||
const noData = ref(true); // 目前顯示「無資料」
|
|
||||||
|
|
||||||
// 確認是否有資料,無則不呼叫 getDashboardTemp 也不顯示 chart
|
const allTempData = ref([]);
|
||||||
|
const currentOptionType = ref(1); // 1 = 溫度,2 = 濕度
|
||||||
|
const noData = ref(true); // 初始先視為無資料,等 API 後再更新
|
||||||
|
const chartRef = ref(null);
|
||||||
|
|
||||||
|
// 監聽建築切換:依 sysConfig 決定是否顯示/取數據
|
||||||
watch(
|
watch(
|
||||||
() => buildingStore.selectedBuilding?.building_guid,
|
() => buildingStore.selectedBuilding?.building_guid,
|
||||||
async (guid) => {
|
async (guid) => {
|
||||||
if (guid) {
|
if (timeoutTimer.value) clearInterval(timeoutTimer.value);
|
||||||
await buildingStore.getSysConfig(guid);
|
allTempData.value = [];
|
||||||
|
noData.value = true;
|
||||||
|
if (!guid) return;
|
||||||
|
|
||||||
|
await buildingStore.getSysConfig(guid);
|
||||||
const showRefrigeration =
|
const showRefrigeration =
|
||||||
buildingStore.sysConfig?.value?.show_refrigeration;
|
buildingStore.sysConfig?.value?.show_refrigeration;
|
||||||
|
|
||||||
if (showRefrigeration === false) {
|
if (showRefrigeration === false) {
|
||||||
noData.value = true; // 不顯示圖表
|
noData.value = true; // 不顯示
|
||||||
return; // 不呼叫 getData
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
noData.value = false; // 有資料才進行呼叫
|
noData.value = false; // 顯示並開始取資料
|
||||||
getData();
|
await getData();
|
||||||
timeoutTimer.value = setInterval(getData, 60000); // 每分鐘叫一次
|
timeoutTimer.value = setInterval(getData, 60_000); // 每分鐘更新
|
||||||
}
|
|
||||||
},
|
},
|
||||||
{ immediate: true }
|
{ 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({
|
const defaultChartOption = ref({
|
||||||
tooltip: {
|
tooltip: { trigger: "axis" },
|
||||||
trigger: "axis",
|
|
||||||
},
|
|
||||||
legend: {
|
legend: {
|
||||||
data: [],
|
data: [],
|
||||||
textStyle: {
|
top: 0, // 靠最上方
|
||||||
color: "#ffffff",
|
textStyle: { color: "#ffffff", fontSize: 12 },
|
||||||
fontSize: 16,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
grid: {
|
grid: {
|
||||||
top: "10%",
|
top: "35%",
|
||||||
left: "0%",
|
left: "0%",
|
||||||
right: "0%",
|
right: "0%",
|
||||||
bottom: "0%",
|
bottom: "0%",
|
||||||
@ -164,40 +75,129 @@ const defaultChartOption = ref({
|
|||||||
series: [],
|
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(() => [
|
const buttonItems = computed(() => [
|
||||||
{ key: 1, title: t("dashboard.temperature"), active: true },
|
{ key: 1, title: t("dashboard.temperature"), active: true },
|
||||||
{ key: 2, title: t("dashboard.humidity"), active: false },
|
{ key: 2, title: t("dashboard.humidity"), active: false },
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
// 語系切換時更新按鈕文案
|
||||||
watch(
|
watch(
|
||||||
() => locale.value,
|
() => locale.value,
|
||||||
() => {
|
() => setItems(buttonItems.value),
|
||||||
setItems(buttonItems.value);
|
|
||||||
},
|
|
||||||
{ immediate: true }
|
{ immediate: true }
|
||||||
);
|
);
|
||||||
|
|
||||||
// 切換 tab(目前無實際資料行為)
|
// 切換溫度/濕度後重取資料並重啟輪詢
|
||||||
watch(
|
watch(
|
||||||
selectedBtn,
|
selectedBtn,
|
||||||
(newValue) => {
|
async (newVal) => {
|
||||||
if (timeoutTimer.value) {
|
if ([1, 2].includes(newVal?.key)) {
|
||||||
clearInterval(timeoutTimer.value);
|
currentOptionType.value = newVal.key;
|
||||||
}
|
|
||||||
|
|
||||||
if ([1, 2].includes(newValue?.key)) {
|
if (buildingStore.sysConfig?.value?.show_refrigeration !== false) {
|
||||||
currentOptionType.value = newValue.key;
|
if (timeoutTimer.value) clearInterval(timeoutTimer.value);
|
||||||
|
await getData();
|
||||||
|
timeoutTimer.value = setInterval(getData, 60_000);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{ immediate: true, deep: true }
|
{ immediate: true, deep: true }
|
||||||
);
|
);
|
||||||
|
|
||||||
// 清除計時器
|
// 限制顯示資料點數
|
||||||
onUnmounted(() => {
|
function sampleData(data = [], maxCount = 30) {
|
||||||
if (timeoutTimer.value) {
|
const len = data.length;
|
||||||
clearInterval(timeoutTimer.value);
|
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);
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@ -205,24 +205,27 @@ onUnmounted(() => {
|
|||||||
<h3 class="text-info text-xl text-center">
|
<h3 class="text-info text-xl text-center">
|
||||||
{{ $t("dashboard.refrig_chart") }}
|
{{ $t("dashboard.refrig_chart") }}
|
||||||
</h3>
|
</h3>
|
||||||
<div className="my-3 w-full flex justify-center relative">
|
|
||||||
|
<div class="w-full flex justify-center relative">
|
||||||
<ButtonConnectedGroup
|
<ButtonConnectedGroup
|
||||||
:items="items"
|
:items="items"
|
||||||
:onclick="(e, item) => changeActiveBtn(item)"
|
:onclick="(e, item) => changeActiveBtn(item)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
v-if="noData"
|
v-if="noData"
|
||||||
class="text-center text-white text-lg min-h-[260px] flex items-center justify-center"
|
class="text-center text-white text-lg min-h-[260px] flex items-center justify-center"
|
||||||
>
|
>
|
||||||
{{ $t("dashboard.no_data") }}
|
{{ $t("dashboard.no_data") }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<LineChart
|
<LineChart
|
||||||
v-if="!noData"
|
v-else
|
||||||
id="dashboard_other_real_temp"
|
id="dashboard_refrigeration_temp"
|
||||||
class="min-h-[260px] max-h-fit"
|
class="min-h-[260px] max-h-fit"
|
||||||
:option="defaultChartOption"
|
:option="defaultChartOption"
|
||||||
ref="indoorChartRef"
|
ref="chartRef"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
276
src/views/rtsp/Rtsp.vue
Normal file
276
src/views/rtsp/Rtsp.vue
Normal file
@ -0,0 +1,276 @@
|
|||||||
|
<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>
|
@ -20,7 +20,7 @@ watch(
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<a-carousel arrows class="mt-5 shadow-lg">
|
<!-- <a-carousel arrows class="mt-5 shadow-lg">
|
||||||
<template #prevArrow>
|
<template #prevArrow>
|
||||||
<div class="custom-slick-arrow" style="left: 10px; z-index: 1">
|
<div class="custom-slick-arrow" style="left: 10px; z-index: 1">
|
||||||
<font-awesome-icon
|
<font-awesome-icon
|
||||||
@ -42,7 +42,7 @@ watch(
|
|||||||
<div v-for="(url, index) in imgData" :key="index">
|
<div v-for="(url, index) in imgData" :key="index">
|
||||||
<img :src="`${FILE_BASEURL}/${url}`" alt="Image" />
|
<img :src="`${FILE_BASEURL}/${url}`" alt="Image" />
|
||||||
</div>
|
</div>
|
||||||
</a-carousel>
|
</a-carousel> -->
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
File diff suppressed because one or more lines are too long
@ -16,7 +16,7 @@ export default defineConfig({
|
|||||||
server: {
|
server: {
|
||||||
proxy: {
|
proxy: {
|
||||||
"/upload": {
|
"/upload": {
|
||||||
target: "https://ibms-empower2.production.mjmtech.com.tw",
|
target: "https://ibms-Empower.production.mjmtech.com.tw",
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
secure: false,
|
secure: false,
|
||||||
},
|
},
|
||||||
@ -42,6 +42,7 @@ export default defineConfig({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
resolve: {
|
resolve: {
|
||||||
|
dedupe: ["vue"],
|
||||||
alias: {
|
alias: {
|
||||||
"@": fileURLToPath(new URL("./src", import.meta.url)),
|
"@": fileURLToPath(new URL("./src", import.meta.url)),
|
||||||
"@ASSET": fileURLToPath(new URL("./src/assets", import.meta.url)),
|
"@ASSET": fileURLToPath(new URL("./src/assets", import.meta.url)),
|
||||||
|
Loading…
Reference in New Issue
Block a user