首頁告警、溫度、2D圖設備狀態更新

This commit is contained in:
koko 2025-07-22 10:53:18 +08:00
parent 6e346af685
commit c48072a41c
7 changed files with 362 additions and 89 deletions

View File

@ -1,6 +1,7 @@
export const POST_ACK_API = `/obix/alarm`;
export const GET_ALERT_FORMID_API = `/Alert/AlertList`;
export const GET_ALERT_LOG_API = `api/Alarm/GetAlarmLog`;
export const GET_ALERT_LOG_LIST_API = `api/Alarm/GetAlarmLogList`;
export const POST_OPERATION_RECORD_API = `/operation/SavOpeRecord`;
export const GET_ALERT_SUB_LIST_API = `api/Device/GetMainSub`;

View File

@ -2,6 +2,7 @@ import {
POST_ACK_API,
GET_ALERT_FORMID_API,
GET_ALERT_LOG_API,
GET_ALERT_LOG_LIST_API,
POST_OPERATION_RECORD_API,
GET_ALERT_SUB_LIST_API,
GET_OUTLIERS_LIST_API,
@ -19,7 +20,7 @@ import {
GET_ALERT_SCHEDULE_LIST_API,
POST_ALERT_SCHEDULE,
DELETE_ALERT_SCHEDULE,
POST_ALERT_MQTT_REFRESH
POST_ALERT_MQTT_REFRESH,
} from "./api";
import instance from "@/util/request";
import apihandler from "@/util/apihandler";
@ -50,6 +51,16 @@ export const getAlertLog = async ({
});
};
export const getAlertLogList = async (building_guid) => {
const res = await instance.post(GET_ALERT_LOG_LIST_API, {
building_guid,
});
return apihandler(res.code, res.data, {
msg: res.msg,
code: res.code,
});
};
export const postOperationRecord = async (formData) => {
const res = await instance.post(POST_OPERATION_RECORD_API, formData);

View File

@ -17,17 +17,40 @@ const props = defineProps({
let chart = ref(null);
let dom = ref(null);
let currentClickPosition = ref([]);
let currentMapName = ref(null);
async function updateSvg(svg, option) {
if (!chart.value && dom.value && svg) {
init();
} else {
clear();
}
axios.get(svg.path).then(({ data }) => {
// SVG
const needNewSvg = currentMapName.value !== svg.full_name;
if (needNewSvg) {
// SVG
console.log("Loading new SVG map:", svg.full_name);
currentMapName.value = svg.full_name;
try {
const { data } = await axios.get(svg.path);
echarts.registerMap(svg.full_name, { svg: data });
chart.value.setOption(option);
if (props.getCoordinate) {
setupClickHandler(option);
} catch (error) {
console.error("Failed to load SVG:", error);
}
} else if (chart.value) {
// SVG
console.log("Updating chart data only");
chart.value.setOption({
series: option.series
}, false, true);
}
}
function setupClickHandler(option) {
if (props.getCoordinate && chart.value) {
chart.value.getZr().on("click", function (params) {
var pixelPoint = [params.offsetX, params.offsetY];
var dataPoint = chart.value.convertFromPixel(
@ -36,7 +59,7 @@ async function updateSvg(svg, option) {
);
currentClickPosition.value = dataPoint;
props.getCoordinate(dataPoint);
const updatedData = option.series.data
const updatedData = option.series[1].data
.filter(
(point) => !(point.itemStyle && point.itemStyle.color === "#0000FF")
)
@ -45,14 +68,12 @@ async function updateSvg(svg, option) {
itemStyle: { color: "#0000FF" }, //
});
chart.value.setOption({
series: {
series: [{}, {
data: updatedData,
},
}]
});
});
}
});
console.log("updateSvg", svg.path);
}
function clear() {

View File

@ -2,12 +2,13 @@
import DashboardFloorBar from "./components/DashboardFloorBar.vue";
import DashboardEffectScatter from "./components/DashboardEffectScatter.vue";
import DashboardSysCard from "./components/DashboardSysCard.vue";
import DashboardTemp from "./components/DashboardTemp.vue";
import DashboardRefrigTemp from "./components/DashboardRefrigTemp.vue";
import DashboardIndoorTemp from "./components/DashboardIndoorTemp.vue";
import DashboardElectricity from "./components/DashboardElectricity.vue";
import DashboardEmission from "./components/DashboardEmission.vue";
import DashboardAlert from "./components/DashboardAlert.vue";
import { computed, inject, ref, watch } from "vue";
import { computed, inject, ref, watch, onMounted, onUnmounted } from "vue";
import useBuildingStore from "@/stores/useBuildingStore";
import { getSystemDevices, getSystemRealTime } from "@/apis/system";
const FILE_BASEURL = import.meta.env.VITE_FILE_API_BASEURL;
@ -15,6 +16,21 @@ const buildingStore = useBuildingStore()
const subscribeData = ref([]);
const systemData = ref({});
let intervalId = null;
//
const startInterval = () => {
//
if (intervalId) {
clearInterval(intervalId);
}
// 5 getData
intervalId = setInterval(() => {
getData();
}, 5000);
};
const getData = async () => {
const res = await getSystemDevices({
building_guid: buildingStore.selectedBuilding?.building_guid,
@ -38,11 +54,15 @@ const getData = async () => {
//
let state = "online";
let bgColor = "rgba(255, 255, 255)";
let bgColor = device.device_normal_color;
if (device.device_status === "offline" || device.device_status === null) {
state = "offline";
bgColor = "rgba(34, 51, 85)";
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 [
@ -69,12 +89,20 @@ watch(
(newBuilding) => {
if (newBuilding) {
getData();
startInterval();
}
},
{
immediate: true
}
);
//
onUnmounted(() => {
if (intervalId) {
clearInterval(intervalId);
}
});
</script>
<template>
@ -83,10 +111,7 @@ watch(
class="order-3 lg:order-1 w-full lg:w-1/4 h-full flex flex-col justify-start z-10 border-dashboard px-12"
>
<div>
<DashboardRefrigTemp />
</div>
<div class="mt-10">
<DashboardIndoorTemp />
<DashboardTemp />
</div>
<div class="mt-10">
<DashboardAlert />

View File

@ -1,44 +1,43 @@
<script setup>
import dayjs from "dayjs";
import { computed, ref, onMounted } from "vue";
import { faker } from "@faker-js/faker"; // faker.js
import { useI18n } from "vue-i18n";
import { ref, watch, onUnmounted } from "vue";
import { getAlertLogList } from "@/apis/alert";
import useBuildingStore from "@/stores/useBuildingStore";
const store = useBuildingStore();
const { t } = useI18n();
//
const fakeAlarmData = ref([]);
const dataSource = ref([]);
let intervalId = null; // setInterval ID
const generateFakeAlarmData = (count = 5) => {
const data = [];
for (let i = 0; i < count; i++) {
data.push({
uuid: faker.string.uuid(),
timestamp_date: faker.date.recent().toISOString(), //
timestamp_time: faker.date
.recent()
.toLocaleTimeString("zh-TW", { hour12: false }), //
full_name: faker.commerce.productName(), //
msg: faker.lorem.sentence(), //
});
}
return data;
const getAlarmData = async (building_guid) => {
const res = await getAlertLogList(building_guid);
dataSource.value = (res.data || []);
};
onMounted(() => {
fakeAlarmData.value = generateFakeAlarmData(); //
});
watch(
() => store.selectedBuilding,
(newBuilding) => {
if (newBuilding) {
getAlarmData(newBuilding.building_guid);
const alarms = computed(() =>
fakeAlarmData.value.slice(0, 5).map((d) => ({
...d,
timestamp_date: dayjs(d.timestamp_date).format("YYYY/MM/DD"),
timestamp_time: d.timestamp_time.split(":").slice(0, 2).join(":"),
}))
intervalId = setInterval(() => {
getAlarmData(newBuilding.building_guid);
}, 30 * 1000);
}
},
{ immediate: true }
);
onUnmounted(() => {
if (intervalId) {
clearInterval(intervalId);
intervalId = null;
}
});
</script>
<template>
<h3 class="text-info text-xl text-center">{{$t("dashboard.alerts_data")}} Top 5</h3>
<h3 class="text-info text-xl text-center">
{{ $t("dashboard.alerts_data") }} Top 5
</h3>
<div class="overflow-x-auto">
<table class="table">
<thead>
@ -50,11 +49,11 @@ const alarms = computed(() =>
</tr>
</thead>
<tbody class="text-base">
<tr v-for="alarm in alarms" :key="alarm.uuid">
<td>{{ alarm.timestamp_date }}</td>
<td>{{ alarm.timestamp_time }}</td>
<td>{{ alarm.full_name }}</td>
<td>{{ alarm.msg }}</td>
<tr v-for="alarm in dataSource" :key="alarm.id">
<td>{{ alarm.year }}</td>
<td>{{ alarm.time }}</td>
<td>{{ alarm.device_number }}</td>
<td>{{ alarm.remark }}</td>
</tr>
</tbody>
</table>

View File

@ -18,6 +18,7 @@ const props = defineProps({
});
const asset_floor_chart = ref(null);
const currentFloorId = ref(null);
const defaultOption = (map, data = []) => {
return {
@ -40,7 +41,7 @@ const defaultOption = (map, data = []) => {
},
map,
roam: true, //
layoutSize: window.innerWidth <= 768 ? "110%" : "75%",
layoutSize: window.innerWidth <= 768 ? "110%" : "100%",
layoutCenter: ["50%", "50%"],
scaleLimit: { min: 1, max: 2 },
},
@ -101,7 +102,12 @@ watch(
[searchParams, () => asset_floor_chart.value, () => props.data],
([newValue, newChart, newData], [oldValue]) => {
if (newValue.floor_id && newChart && Object.keys(newData || {}).length > 0) {
console.log("Updating chart with new data", newValue, newChart, newData);
const isFloorChanged = currentFloorId.value !== newValue.floor_id;
if (isFloorChanged) {
// SVG
console.log("Floor changed, updating chart with new SVG", newValue.floor_id);
currentFloorId.value = newValue.floor_id;
newChart.updateSvg(
{
full_name: newValue.floor_id,
@ -109,6 +115,13 @@ watch(
},
defaultOption(newValue.floor_id, currentIconData.value)
);
} else if (currentFloorId.value === newValue.floor_id && newChart.chart) {
// SVG
console.log("Data updated, refreshing chart data only");
newChart.chart.setOption({
series: defaultOption(newValue.floor_id, currentIconData.value).series
}, false, true);
}
}
},
{

View File

@ -0,0 +1,203 @@
<script setup>
import LineChart from "@/components/chart/LineChart.vue";
import { SECOND_CHART_COLOR } from "@/constant";
import dayjs from "dayjs";
import { ref, watch, onUnmounted } from "vue";
import useActiveBtn from "@/hooks/useActiveBtn";
import { getDashboardTemp } from "@/apis/dashboard";
import useSearchParams from "@/hooks/useSearchParam";
import useBuildingStore from "@/stores/useBuildingStore";
const { searchParams } = useSearchParams();
const buildingStore = useBuildingStore();
const intervalType = "immediateTemp";
const timeoutTimer = ref("");
const { items, changeActiveBtn, setItems, selectedBtn } = useActiveBtn();
const data = ref([]);
const other_real_temp_chart = ref(null);
const defaultChartOption = ref({
tooltip: {
trigger: "axis",
},
legend: {
data: [],
textStyle: {
color: "#ffffff",
fontSize: 16,
},
},
grid: {
top: "10%",
left: "0%",
right: "0%",
bottom: "0%",
containLabel: true,
},
xAxis: {
// type: 'time',
type: "category",
splitLine: {
show: false,
},
axisLabel: {
color: "#ffffff",
},
data: [],
},
yAxis: {
type: "value",
splitLine: {
show: false,
},
axisLabel: {
color: "#ffffff",
},
},
series: [],
});
const getData = async (tempOption) => {
const res = await getDashboardTemp({
building_guid: buildingStore.selectedBuilding.building_guid,
tempOption, // 1 2:
timeInterval: 1, // =>1.4.8
});
if (res.isSuccess) {
if (tempOption === 1) {
console.log("室內溫度資料:", res.data["室溫"]);
data.value = res.data["室溫"] || [];
} else {
console.log("冷藏溫度資料:", res.data["冷藏溫度"]);
data.value = res.data["冷藏溫度"] || [];
}
}
};
//
watch(
() => buildingStore.selectedBuilding?.building_guid,
(newBuildingGuid) => {
if (newBuildingGuid) {
getData(1);
timeoutTimer.value = setInterval(() => {
getData(1);
}, 60 * 1000);
} else {
//
if (timeoutTimer.value) {
clearInterval(timeoutTimer.value);
}
}
setItems([
{
title: "室內溫度",
key: 1,
active: true,
},
{
title: "冷藏溫度",
key: 2,
active: false,
},
]);
},
{
immediate: true,
}
);
watch(
selectedBtn,
(newValue) => {
if (timeoutTimer.value) {
clearInterval(timeoutTimer.value);
}
if (newValue?.key) {
getData(newValue.key);
//
timeoutTimer.value = setInterval(() => {
getData(newValue.key);
}, 60 * 1000);
}
},
{
deep: true,
}
);
watch(
data,
(newValue) => {
if (newValue?.length > 0 && other_real_temp_chart.value?.chart) {
const firstItem = newValue[0];
if (firstItem?.data?.length > 0) {
const validData = firstItem.data.filter((item) => item.value !== null && item.value !== undefined);
if (validData.length > 0) {
const minValue = Math.min(...validData.map(({ value }) => value));
const maxValue = Math.max(...validData.map(({ value }) => value));
other_real_temp_chart.value.chart.setOption({
legend: {
data: newValue.map(({ full_name }) => full_name),
},
xAxis: {
data: firstItem.data.map(({ time }) => time), // 使 time
},
yAxis: {
min: Math.floor(minValue),
max: Math.ceil(maxValue),
},
series: newValue.map(({ full_name, data }, index) => ({
name: full_name,
type: "line",
data: data.map(({ value }) => value),
showSymbol: false,
itemStyle: {
color: SECOND_CHART_COLOR[index % SECOND_CHART_COLOR.length],
},
})),
});
}
}
}
},
{ deep: true }
);
onUnmounted(() => {
//
if (timeoutTimer.value) {
clearInterval(timeoutTimer.value);
}
});
</script>
<template>
<div class="flex flex-col justify-center mb-3">
<h3 class="text-info font-bold text-xl text-center">溫度趨勢</h3>
<div className="mt-2 w-full flex justify-center relative">
<ButtonConnectedGroup
:items="items"
:onclick="
(e, item) => {
changeActiveBtn(item);
}
"
/>
</div>
</div>
<LineChart
id="dashboard_other_real_temp"
class="min-h-[350px] max-h-fit"
:option="defaultChartOption"
ref="other_real_temp_chart"
/>
</template>
<style lang="scss" scoped></style>