fix: dashboard 冷藏圖表顯示問題
This commit is contained in:
parent
b7d4ef5a62
commit
759d353104
@ -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">
|
||||||
|
<DashboardIndoor />
|
||||||
<!-- <DashboardProduct
|
</div>
|
||||||
@visible-change="(v) => (productVisible = v)"
|
<div class="flex flex-col gap-5">
|
||||||
v-show="productVisible"
|
<DashboardRefrig />
|
||||||
/>
|
</div>
|
||||||
<DashboardProductComplete
|
|
||||||
@visible-change="(v) => (productCompleteVisible = v)"
|
|
||||||
v-show="productVisible"
|
|
||||||
/> -->
|
|
||||||
<DashboardIndoor />
|
|
||||||
<DashboardRefrig class="mb-10" />
|
|
||||||
</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>
|
||||||
|
@ -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;
|
||||||
|
|
||||||
const showRefrigeration =
|
await buildingStore.getSysConfig(guid);
|
||||||
buildingStore.sysConfig?.value?.show_refrigeration;
|
const showRefrigeration =
|
||||||
|
buildingStore.sysConfig?.value?.show_refrigeration;
|
||||||
|
|
||||||
if (showRefrigeration === false) {
|
if (showRefrigeration === false) {
|
||||||
noData.value = true; // 不顯示圖表
|
noData.value = true; // 不顯示
|
||||||
return; // 不呼叫 getData
|
return;
|
||||||
}
|
|
||||||
|
|
||||||
noData.value = false; // 有資料才進行呼叫
|
|
||||||
getData();
|
|
||||||
timeoutTimer.value = setInterval(getData, 60000); // 每分鐘叫一次
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
noData.value = false; // 顯示並開始取資料
|
||||||
|
await getData();
|
||||||
|
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>
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user