diff --git a/src/config/cn.json b/src/config/cn.json index 035889b..f46341e 100644 --- a/src/config/cn.json +++ b/src/config/cn.json @@ -193,7 +193,7 @@ "time": "发生时间", "error_msg": "异常原因", "ack_state": "Ack 确认", - "repair_order_number": "派工 / 维运单号", + "repair_order_number": "烟 / 火警处理单", "repair_order": "维修单", "form_number": "表单编号", "start_time": "预计开始时间", @@ -426,9 +426,8 @@ }, "rtsp": { "title": "影像串流", - - "selectPath": "选择存储位置", "selectDevice": "选择设备", + "pleaseSelectDevice": "请先选择设备", "normalQuery": "已复归查询" } } diff --git a/src/config/tw.json b/src/config/tw.json index 0f539d8..f8afc8b 100644 --- a/src/config/tw.json +++ b/src/config/tw.json @@ -193,7 +193,7 @@ "time": "發生時間", "error_msg": "異常原因", "ack_state": "Ack 確認", - "repair_order_number": "派工 / 維運單號", + "repair_order_number": "煙 / 火告警處理單", "repair_order": "維修單", "form_number": "表單編號", "start_time": "預計開始時間", @@ -427,6 +427,7 @@ "rtsp": { "title": "影像串流", "selectDevice": "選擇設備", + "pleaseSelectDevice": "請先選擇設備", "normalQuery": "已復歸查詢" } } diff --git a/src/config/us.json b/src/config/us.json index 30a51ed..a8248d9 100644 --- a/src/config/us.json +++ b/src/config/us.json @@ -427,6 +427,7 @@ "rtsp": { "title": "Video Stream", "selectDevice": "Select Device", + "pleaseSelectDevice": "Please select a device first", "normalQuery": "Query normal records" } } diff --git a/src/views/dashboard/components/DashboardIndoor.vue b/src/views/dashboard/components/DashboardIndoor.vue index 7b90ece..13656b4 100644 --- a/src/views/dashboard/components/DashboardIndoor.vue +++ b/src/views/dashboard/components/DashboardIndoor.vue @@ -2,7 +2,7 @@ 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, onUnmounted, computed, nextTick, onMounted } from "vue"; import useActiveBtn from "@/hooks/useActiveBtn"; import { getDashboardTemp } from "@/apis/dashboard"; import useSearchParams from "@/hooks/useSearchParam"; @@ -12,16 +12,42 @@ import { useI18n } from "vue-i18n"; const { t, locale } = useI18n(); const { searchParams } = useSearchParams(); const buildingStore = useBuildingStore(); -const timeoutTimer = ref(null); // 定時器參考 +const timeoutTimer = ref(null); const { items, changeActiveBtn, setItems, selectedBtn } = useActiveBtn(); const allTempData = ref([]); -const currentOptionType = ref(1); // 當前顯示類型:1 = 溫度,2 = 濕度 -const noData = ref(false); // 無資料顯示控制 +const currentOptionType = ref(1); // 1=溫度, 2=濕度 +const noData = ref(false); const indoorChartRef = ref(null); -// 確認是否有資料,無則不呼叫 getDashboardTemp 也不顯示 chart +// 雙類型資料快取 +const cacheMap = ref({ 1: [], 2: [] }); + +function getChart() { + return indoorChartRef.value?.chart ?? null; +} + +async function resizeChart() { + await nextTick(); + const chart = getChart(); + if (chart) { + try { chart.resize(); } catch {} + } +} + +onMounted(() => { + window.addEventListener("resize", resizeChart); + setItems(buttonItems.value); + resizeChart(); +}); + +onUnmounted(() => { + window.removeEventListener("resize", resizeChart); + if (timeoutTimer.value) clearInterval(timeoutTimer.value); +}); + +// 初次或建物切換 → 先檢查 show_room,再一次載入兩種資料到快取 watch( () => buildingStore.selectedBuilding?.building_guid, async (guid) => { @@ -34,195 +60,185 @@ watch( if (showRoom === false) { noData.value = true; - return; // 不呼叫 getData + return; } - // 允許顯示圖表+呼叫資料 noData.value = false; - getData(); - timeoutTimer.value = setInterval(getData, 60000); // 每分鐘自動更新 + + // 一次預載「溫度+濕度」資料 + await preloadBothOptions(); + + // 預設顯示溫度(立即用快取畫) + currentOptionType.value = 1; + applyChartFromCache(1); // 立即顯示 + await resizeChart(); + + // 之後每分鐘更新兩種快取,並同步更新目前顯示的那個 + timeoutTimer.value = setInterval(async () => { + await preloadBothOptions(false); + applyChartFromCache(currentOptionType.value); // 保持即時 + }, 60000); }, { immediate: true } ); -// 預設圖表設定 +// 預設圖表 const defaultChartOption = ref({ tooltip: { trigger: "axis" }, - legend: { - data: [], - top: 0, // 靠最上方 - textStyle: { color: "#ffffff", fontSize: 12 }, - }, - grid: { - top: "35%", - left: "0%", - right: "0%", - bottom: "0%", - containLabel: true, - }, - xAxis: { - type: "category", - splitLine: { show: false }, - axisLabel: { color: "#ffffff" }, - data: [], - }, - yAxis: { - type: "value", - splitLine: { show: false }, - axisLabel: { color: "#ffffff" }, - }, + legend: { data: [], top: 0, textStyle: { color: "#ffffff", fontSize: 12 } }, + grid: { top: "35%", left: "0%", right: "0%", bottom: "0%", containLabel: true }, + xAxis: { type: "category", splitLine: { show: false }, axisLabel: { color: "#ffffff" }, data: [] }, + yAxis: { type: "value", splitLine: { show: false }, axisLabel: { color: "#ffffff" } }, series: [], }); -const getData = async () => { +// 取得單一類型資料 +async function fetchOption(option) { const buildingGuid = buildingStore.selectedBuilding?.building_guid; - if (!buildingGuid) return; - + if (!buildingGuid) return []; try { const res = await getDashboardTemp({ building_guid: buildingGuid, - tempOption: 1, // 室溫區域 + tempOption: 1, timeInterval: 1, - option: currentOptionType.value, + option, // 1=溫度, 2=濕度 }); - const key = "室溫"; - allTempData.value = res.isSuccess ? res.data?.[key] ?? [] : []; - noData.value = allTempData.value.length === 0; + return res?.isSuccess ? res?.data?.[key] ?? [] : []; } catch (e) { console.error("getDashboardTemp error", e); - allTempData.value = []; - noData.value = true; + return []; } -}; +} + +// 載入兩種資料到快取(首次 parallel,後續也可 parallel) +async function preloadBothOptions(resetNoData = true) { + const [tData, hData] = await Promise.all([fetchOption(1), fetchOption(2)]); + cacheMap.value[1] = tData; + cacheMap.value[2] = hData; + + if (resetNoData) { + // 兩邊都空才算無資料 + noData.value = tData.length === 0 && hData.length === 0; + } +} + +// 立即用快取資料更新圖表(無需等待 API) +function applyChartFromCache(option) { + const data = cacheMap.value[option] || []; + if (!data.length) { + // 沒快取就顯示無資料(或保留上一個畫面也行) + noData.value = true; + return; + } + noData.value = false; + renderChart(data); +} + +// 把資料轉成 ECharts option(抽成函數供快取與即時更新共用) +function renderChart(newVal) { + const chart = getChart(); + if (!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 minVal = Math.min(...allValues); + const maxVal = Math.max(...allValues); + const yMin = Math.floor(minVal) - 1; + const yMax = Math.ceil(maxVal) + 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] }, + })), + }); +} -// 溫度與濕度切換按鈕 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), { immediate: true }); -// 多語系切換時更新按鈕文字 -watch( - () => locale.value, - () => setItems(buttonItems.value), - { immediate: true } -); - -// 切換溫度/濕度按鈕後更新資料與啟動定時器 -// 切換溫度/濕度按鈕 +// 切換溫/濕:先「立即」用快取畫,再刷新快取(回來會自動更即時) watch( selectedBtn, - (newVal) => { - if ([1, 2].includes(newVal?.key)) { - currentOptionType.value = newVal.key; + async (newVal) => { + if (![1, 2].includes(newVal?.key)) return; - // 再次確認 show_room 為 true 才重新取資料 - if (buildingStore.sysConfig?.value?.show_room) { - getData(); - if (timeoutTimer.value) clearInterval(timeoutTimer.value); - timeoutTimer.value = setInterval(getData, 60000); - } + currentOptionType.value = newVal.key; + + // 1) 立刻顯示:直接用快取更新圖表 + applyChartFromCache(currentOptionType.value); + await resizeChart(); + + // 2) 之後再更新快取(不阻塞顯示) + const fresh = await fetchOption(currentOptionType.value); + if (fresh.length) { + cacheMap.value[currentOptionType.value] = fresh; + // 當前頁籤就地更新到最新版 + renderChart(fresh); + } else { + // 若拿不到新資料,不覆寫舊快取,維持畫面 } }, { 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]); - } - + for (let i = 0; i < maxCount; i++) sampled.push(data[Math.round(i * step)]); return sampled; } - -// 更新圖表資料 -watch( - allTempData, - (newVal) => { - if (!newVal?.length || !indoorChartRef.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 minVal = Math.min(...allValues); - const maxVal = Math.max(...allValues); - - const yMin = Math.floor(minVal) - 1; - const yMax = Math.ceil(maxVal) + 1; - - indoorChartRef.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 } -); -// 離開元件時清除定時器 -onUnmounted(() => { - if (timeoutTimer.value) clearInterval(timeoutTimer.value); -}); - - diff --git a/src/views/rtsp/Rtsp.vue b/src/views/rtsp/Rtsp.vue index fa4cc6d..f8058b2 100644 --- a/src/views/rtsp/Rtsp.vue +++ b/src/views/rtsp/Rtsp.vue @@ -7,7 +7,7 @@ import AlertSearchTimeRange from "@/views/alert/components/AlertQuery/AlertSearc import Table from "@/components/customUI/Table.vue"; const DEFAULT_MONITOR_URL = - "http://192.168.0.219:8026/?url=rtsp://admin02:mjmAdmin_99@192.168.0.200:554/stream1"; + "https://ibms-ils-rtsp.production.mjmtech.com.tw/?url=rtsp://admin02:mjmAdmin_99@192.168.0.200:554/stream1"; export default { name: "Rtsp",