fix: dashboard 冷藏圖表顯示問題

This commit is contained in:
MJM_2025_05\polly 2025-09-19 09:38:22 +08:00
parent b7d4ef5a62
commit 759d353104
3 changed files with 158 additions and 158 deletions

View File

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

View File

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

View File

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