首頁告警、溫度、2D圖設備狀態更新
This commit is contained in:
parent
6e346af685
commit
c48072a41c
@ -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`;
|
||||
|
@ -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);
|
||||
|
||||
|
@ -17,42 +17,63 @@ 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 }) => {
|
||||
echarts.registerMap(svg.full_name, { svg: data });
|
||||
chart.value.setOption(option);
|
||||
if (props.getCoordinate) {
|
||||
chart.value.getZr().on("click", function (params) {
|
||||
var pixelPoint = [params.offsetX, params.offsetY];
|
||||
var dataPoint = chart.value.convertFromPixel(
|
||||
{ geoIndex: 0 },
|
||||
pixelPoint
|
||||
);
|
||||
currentClickPosition.value = dataPoint;
|
||||
props.getCoordinate(dataPoint);
|
||||
const updatedData = option.series.data
|
||||
.filter(
|
||||
(point) => !(point.itemStyle && point.itemStyle.color === "#0000FF")
|
||||
)
|
||||
.concat({
|
||||
value: dataPoint, // 當前座標值
|
||||
itemStyle: { color: "#0000FF" }, // 設為藍色
|
||||
});
|
||||
chart.value.setOption({
|
||||
series: {
|
||||
data: updatedData,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
// 檢查是否需要載入新的 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);
|
||||
setupClickHandler(option);
|
||||
} catch (error) {
|
||||
console.error("Failed to load SVG:", error);
|
||||
}
|
||||
});
|
||||
console.log("updateSvg", svg.path);
|
||||
} 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(
|
||||
{ geoIndex: 0 },
|
||||
pixelPoint
|
||||
);
|
||||
currentClickPosition.value = dataPoint;
|
||||
props.getCoordinate(dataPoint);
|
||||
const updatedData = option.series[1].data
|
||||
.filter(
|
||||
(point) => !(point.itemStyle && point.itemStyle.color === "#0000FF")
|
||||
)
|
||||
.concat({
|
||||
value: dataPoint, // 當前座標值
|
||||
itemStyle: { color: "#0000FF" }, // 設為藍色
|
||||
});
|
||||
chart.value.setOption({
|
||||
series: [{}, {
|
||||
data: updatedData,
|
||||
}]
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function clear() {
|
||||
|
@ -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 />
|
||||
|
@ -1,60 +1,59 @@
|
||||
<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>
|
||||
<tr class="border-b-2 border-info text-base">
|
||||
<th>{{$t("operation.date")}}</th>
|
||||
<th>{{$t("operation.time")}}</th>
|
||||
<th>{{$t("operation.device_name")}}</th>
|
||||
<th>{{$t("operation.remark")}}</th>
|
||||
<th>{{ $t("operation.date") }}</th>
|
||||
<th>{{ $t("operation.time") }}</th>
|
||||
<th>{{ $t("operation.device_name") }}</th>
|
||||
<th>{{ $t("operation.remark") }}</th>
|
||||
</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>
|
||||
|
@ -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,14 +102,26 @@ 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);
|
||||
newChart.updateSvg(
|
||||
{
|
||||
full_name: newValue.floor_id,
|
||||
path: `${FILE_BASEURL}/upload/floor_map/${newValue.floor_id}.svg`,
|
||||
},
|
||||
defaultOption(newValue.floor_id, currentIconData.value)
|
||||
);
|
||||
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,
|
||||
path: `${FILE_BASEURL}/upload/floor_map/${newValue.floor_id}.svg`,
|
||||
},
|
||||
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);
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
|
203
src/views/dashboard/components/DashboardTemp.vue
Normal file
203
src/views/dashboard/components/DashboardTemp.vue
Normal 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>
|
Loading…
Reference in New Issue
Block a user