fix: 修正首頁左上室內 tabs 切換緩慢問題
This commit is contained in:
parent
f990c302e5
commit
3750e3c124
@ -193,7 +193,7 @@
|
|||||||
"time": "发生时间",
|
"time": "发生时间",
|
||||||
"error_msg": "异常原因",
|
"error_msg": "异常原因",
|
||||||
"ack_state": "Ack 确认",
|
"ack_state": "Ack 确认",
|
||||||
"repair_order_number": "派工 / 维运单号",
|
"repair_order_number": "烟 / 火警处理单",
|
||||||
"repair_order": "维修单",
|
"repair_order": "维修单",
|
||||||
"form_number": "表单编号",
|
"form_number": "表单编号",
|
||||||
"start_time": "预计开始时间",
|
"start_time": "预计开始时间",
|
||||||
@ -426,9 +426,8 @@
|
|||||||
},
|
},
|
||||||
"rtsp": {
|
"rtsp": {
|
||||||
"title": "影像串流",
|
"title": "影像串流",
|
||||||
|
|
||||||
"selectPath": "选择存储位置",
|
|
||||||
"selectDevice": "选择设备",
|
"selectDevice": "选择设备",
|
||||||
|
"pleaseSelectDevice": "请先选择设备",
|
||||||
"normalQuery": "已复归查询"
|
"normalQuery": "已复归查询"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -193,7 +193,7 @@
|
|||||||
"time": "發生時間",
|
"time": "發生時間",
|
||||||
"error_msg": "異常原因",
|
"error_msg": "異常原因",
|
||||||
"ack_state": "Ack 確認",
|
"ack_state": "Ack 確認",
|
||||||
"repair_order_number": "派工 / 維運單號",
|
"repair_order_number": "煙 / 火告警處理單",
|
||||||
"repair_order": "維修單",
|
"repair_order": "維修單",
|
||||||
"form_number": "表單編號",
|
"form_number": "表單編號",
|
||||||
"start_time": "預計開始時間",
|
"start_time": "預計開始時間",
|
||||||
@ -427,6 +427,7 @@
|
|||||||
"rtsp": {
|
"rtsp": {
|
||||||
"title": "影像串流",
|
"title": "影像串流",
|
||||||
"selectDevice": "選擇設備",
|
"selectDevice": "選擇設備",
|
||||||
|
"pleaseSelectDevice": "請先選擇設備",
|
||||||
"normalQuery": "已復歸查詢"
|
"normalQuery": "已復歸查詢"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -427,6 +427,7 @@
|
|||||||
"rtsp": {
|
"rtsp": {
|
||||||
"title": "Video Stream",
|
"title": "Video Stream",
|
||||||
"selectDevice": "Select Device",
|
"selectDevice": "Select Device",
|
||||||
|
"pleaseSelectDevice": "Please select a device first",
|
||||||
"normalQuery": "Query normal records"
|
"normalQuery": "Query normal records"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
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 dayjs from "dayjs";
|
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 useActiveBtn from "@/hooks/useActiveBtn";
|
||||||
import { getDashboardTemp } from "@/apis/dashboard";
|
import { getDashboardTemp } from "@/apis/dashboard";
|
||||||
import useSearchParams from "@/hooks/useSearchParam";
|
import useSearchParams from "@/hooks/useSearchParam";
|
||||||
@ -12,16 +12,42 @@ import { useI18n } from "vue-i18n";
|
|||||||
const { t, locale } = useI18n();
|
const { t, locale } = useI18n();
|
||||||
const { searchParams } = useSearchParams();
|
const { searchParams } = useSearchParams();
|
||||||
const buildingStore = useBuildingStore();
|
const buildingStore = useBuildingStore();
|
||||||
const timeoutTimer = ref(null); // 定時器參考
|
|
||||||
|
|
||||||
|
const timeoutTimer = ref(null);
|
||||||
const { items, changeActiveBtn, setItems, selectedBtn } = useActiveBtn();
|
const { items, changeActiveBtn, setItems, selectedBtn } = useActiveBtn();
|
||||||
|
|
||||||
const allTempData = ref([]);
|
const allTempData = ref([]);
|
||||||
const currentOptionType = ref(1); // 當前顯示類型:1 = 溫度,2 = 濕度
|
const currentOptionType = ref(1); // 1=溫度, 2=濕度
|
||||||
const noData = ref(false); // 無資料顯示控制
|
const noData = ref(false);
|
||||||
const indoorChartRef = ref(null);
|
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(
|
watch(
|
||||||
() => buildingStore.selectedBuilding?.building_guid,
|
() => buildingStore.selectedBuilding?.building_guid,
|
||||||
async (guid) => {
|
async (guid) => {
|
||||||
@ -34,121 +60,85 @@ watch(
|
|||||||
|
|
||||||
if (showRoom === false) {
|
if (showRoom === false) {
|
||||||
noData.value = true;
|
noData.value = true;
|
||||||
return; // 不呼叫 getData
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 允許顯示圖表+呼叫資料
|
|
||||||
noData.value = false;
|
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 }
|
{ immediate: true }
|
||||||
);
|
);
|
||||||
|
|
||||||
// 預設圖表設定
|
// 預設圖表
|
||||||
const defaultChartOption = ref({
|
const defaultChartOption = ref({
|
||||||
tooltip: { trigger: "axis" },
|
tooltip: { trigger: "axis" },
|
||||||
legend: {
|
legend: { data: [], top: 0, textStyle: { color: "#ffffff", fontSize: 12 } },
|
||||||
data: [],
|
grid: { top: "35%", left: "0%", right: "0%", bottom: "0%", containLabel: true },
|
||||||
top: 0, // 靠最上方
|
xAxis: { type: "category", splitLine: { show: false }, axisLabel: { color: "#ffffff" }, data: [] },
|
||||||
textStyle: { color: "#ffffff", fontSize: 12 },
|
yAxis: { type: "value", splitLine: { show: false }, axisLabel: { color: "#ffffff" } },
|
||||||
},
|
|
||||||
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: [],
|
series: [],
|
||||||
});
|
});
|
||||||
|
|
||||||
const getData = async () => {
|
// 取得單一類型資料
|
||||||
|
async function fetchOption(option) {
|
||||||
const buildingGuid = buildingStore.selectedBuilding?.building_guid;
|
const buildingGuid = buildingStore.selectedBuilding?.building_guid;
|
||||||
if (!buildingGuid) return;
|
if (!buildingGuid) return [];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await getDashboardTemp({
|
const res = await getDashboardTemp({
|
||||||
building_guid: buildingGuid,
|
building_guid: buildingGuid,
|
||||||
tempOption: 1, // 室溫區域
|
tempOption: 1,
|
||||||
timeInterval: 1,
|
timeInterval: 1,
|
||||||
option: currentOptionType.value,
|
option, // 1=溫度, 2=濕度
|
||||||
});
|
});
|
||||||
|
|
||||||
const key = "室溫";
|
const key = "室溫";
|
||||||
allTempData.value = res.isSuccess ? res.data?.[key] ?? [] : [];
|
return res?.isSuccess ? res?.data?.[key] ?? [] : [];
|
||||||
noData.value = allTempData.value.length === 0;
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("getDashboardTemp error", e);
|
console.error("getDashboardTemp error", e);
|
||||||
allTempData.value = [];
|
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;
|
noData.value = true;
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
};
|
noData.value = false;
|
||||||
|
renderChart(data);
|
||||||
// 溫度與濕度切換按鈕
|
|
||||||
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(
|
|
||||||
selectedBtn,
|
|
||||||
(newVal) => {
|
|
||||||
if ([1, 2].includes(newVal?.key)) {
|
|
||||||
currentOptionType.value = newVal.key;
|
|
||||||
|
|
||||||
// 再次確認 show_room 為 true 才重新取資料
|
|
||||||
if (buildingStore.sysConfig?.value?.show_room) {
|
|
||||||
getData();
|
|
||||||
if (timeoutTimer.value) clearInterval(timeoutTimer.value);
|
|
||||||
timeoutTimer.value = setInterval(getData, 60000);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{ 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;
|
// 把資料轉成 ECharts option(抽成函數供快取與即時更新共用)
|
||||||
}
|
function renderChart(newVal) {
|
||||||
|
const chart = getChart();
|
||||||
// 更新圖表資料
|
if (!chart) return;
|
||||||
watch(
|
|
||||||
allTempData,
|
|
||||||
(newVal) => {
|
|
||||||
if (!newVal?.length || !indoorChartRef.value?.chart) return;
|
|
||||||
|
|
||||||
const firstValid = newVal.find((d) => d.data?.length);
|
const firstValid = newVal.find((d) => d.data?.length);
|
||||||
if (!firstValid) return;
|
if (!firstValid) return;
|
||||||
@ -166,63 +156,89 @@ watch(
|
|||||||
|
|
||||||
const minVal = Math.min(...allValues);
|
const minVal = Math.min(...allValues);
|
||||||
const maxVal = Math.max(...allValues);
|
const maxVal = Math.max(...allValues);
|
||||||
|
|
||||||
const yMin = Math.floor(minVal) - 1;
|
const yMin = Math.floor(minVal) - 1;
|
||||||
const yMax = Math.ceil(maxVal) + 1;
|
const yMax = Math.ceil(maxVal) + 1;
|
||||||
|
|
||||||
indoorChartRef.value.chart.setOption({
|
chart.setOption({
|
||||||
legend: {
|
legend: { data: newVal.map((d) => d.full_name) },
|
||||||
data: newVal.map((d) => d.full_name),
|
xAxis: { data: sampledXAxis },
|
||||||
},
|
yAxis: { min: yMin, max: yMax },
|
||||||
xAxis: {
|
|
||||||
data: sampledXAxis,
|
|
||||||
},
|
|
||||||
yAxis: {
|
|
||||||
min: yMin,
|
|
||||||
max: yMax,
|
|
||||||
},
|
|
||||||
series: newVal.map((d, i) => ({
|
series: newVal.map((d, i) => ({
|
||||||
name: d.full_name,
|
name: d.full_name,
|
||||||
type: "line",
|
type: "line",
|
||||||
data: sampleData(d.data).map(({ value }) => value),
|
data: sampleData(d.data).map(({ value }) => value),
|
||||||
showSymbol: false,
|
showSymbol: false,
|
||||||
itemStyle: {
|
itemStyle: { color: SECOND_CHART_COLOR[i % SECOND_CHART_COLOR.length] },
|
||||||
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(
|
||||||
|
selectedBtn,
|
||||||
|
async (newVal) => {
|
||||||
|
if (![1, 2].includes(newVal?.key)) return;
|
||||||
|
|
||||||
|
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 {
|
||||||
|
// 若拿不到新資料,不覆寫舊快取,維持畫面
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{ deep: true }
|
{ immediate: true, deep: true }
|
||||||
);
|
);
|
||||||
// 離開元件時清除定時器
|
|
||||||
onUnmounted(() => {
|
// 簡單取樣避免卡頓
|
||||||
if (timeoutTimer.value) clearInterval(timeoutTimer.value);
|
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++) sampled.push(data[Math.round(i * step)]);
|
||||||
|
return sampled;
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<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 class="w-full flex justify-center items-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)"
|
||||||
/>
|
/>
|
||||||
</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_other_real_temp"
|
||||||
class="min-h-[260px] max-h-fit"
|
class="min-h-[260px] max-h-fit"
|
||||||
:option="defaultChartOption"
|
:option="defaultChartOption"
|
||||||
ref="indoorChartRef"
|
ref="indoorChartRef"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss" scoped></style>
|
|
||||||
|
@ -7,7 +7,7 @@ import AlertSearchTimeRange from "@/views/alert/components/AlertQuery/AlertSearc
|
|||||||
import Table from "@/components/customUI/Table.vue";
|
import Table from "@/components/customUI/Table.vue";
|
||||||
|
|
||||||
const DEFAULT_MONITOR_URL =
|
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 {
|
export default {
|
||||||
name: "Rtsp",
|
name: "Rtsp",
|
||||||
|
Loading…
Reference in New Issue
Block a user