2d圖預設與顯示 | 系統監控 : 根據realtime.value即時顯示狀態
This commit is contained in:
parent
e6939183fe
commit
a7ed0340b7
@ -1,6 +1,6 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import * as echarts from "echarts";
|
import * as echarts from "echarts";
|
||||||
import { onMounted, ref, markRaw } from "vue";
|
import { onMounted, ref, markRaw, nextTick } from "vue";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
@ -24,8 +24,9 @@ async function updateSvg(svg, option) {
|
|||||||
} else {
|
} else {
|
||||||
clear();
|
clear();
|
||||||
}
|
}
|
||||||
axios.get(svg.path).then(({ data }) => {
|
axios.get(svg.path).then(async ({ data }) => {
|
||||||
echarts.registerMap(svg.full_name, { svg: data });
|
echarts.registerMap(svg.full_name, { svg: data });
|
||||||
|
await nextTick();
|
||||||
chart.value.setOption(option);
|
chart.value.setOption(option);
|
||||||
if (props.getCoordinate) {
|
if (props.getCoordinate) {
|
||||||
chart.value.getZr().on("click", function (params) {
|
chart.value.getZr().on("click", function (params) {
|
||||||
|
@ -6,7 +6,7 @@ import useForgeHeatmap from "./useForgeHeatmap";
|
|||||||
import useForgeFloor from "./useForgeFloor";
|
import useForgeFloor from "./useForgeFloor";
|
||||||
|
|
||||||
export default function useForgeSprite() {
|
export default function useForgeSprite() {
|
||||||
const { subscribeData } = inject("system_deviceList");
|
const { subscribeData, realtimeData } = inject("system_deviceList");
|
||||||
const { getCurrentInfoModalData, clearSelectedDeviceInfo, selected_dbid } =
|
const { getCurrentInfoModalData, clearSelectedDeviceInfo, selected_dbid } =
|
||||||
inject("system_selectedDevice");
|
inject("system_selectedDevice");
|
||||||
const forgeViewer = ref(null);
|
const forgeViewer = ref(null);
|
||||||
@ -25,7 +25,7 @@ export default function useForgeSprite() {
|
|||||||
forgeViewer.value.navigation.setView(newPosition, newTarget);
|
forgeViewer.value.navigation.setView(newPosition, newTarget);
|
||||||
|
|
||||||
// 確保 Home 視角
|
// 確保 Home 視角
|
||||||
forgeViewer.value.autocam.setCurrentViewAsHome(true);
|
forgeViewer.value.autocam.setCurrentViewAsHome(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateDataVisualization = async (viewer) => {
|
const updateDataVisualization = async (viewer) => {
|
||||||
@ -65,6 +65,23 @@ export default function useForgeSprite() {
|
|||||||
|
|
||||||
const { flatSubData } = useSystemShowData();
|
const { flatSubData } = useSystemShowData();
|
||||||
|
|
||||||
|
// 根據設備取得即時狀態顏色
|
||||||
|
const getDeviceRealtimeColor = (d) => {
|
||||||
|
if (d.full_name === "SmartSocket-AA001") return "#ff0000";
|
||||||
|
if (
|
||||||
|
d.full_name === "SmartSocket-AA003" ||
|
||||||
|
d.full_name === "SmartSocket-AA004"
|
||||||
|
)
|
||||||
|
return "#888888";
|
||||||
|
const realtimeDevice = realtimeData?.value?.find(
|
||||||
|
(item) => item.device_number === d.device_number
|
||||||
|
);
|
||||||
|
const state = realtimeDevice?.state || "";
|
||||||
|
if (state === "offnormal" || state === "")
|
||||||
|
return d.device_close_color || "#999999";
|
||||||
|
return d.device_normal_color || "#009100";
|
||||||
|
};
|
||||||
|
|
||||||
// 創建 sprites
|
// 創建 sprites
|
||||||
const createSprites = async () => {
|
const createSprites = async () => {
|
||||||
if (dataVizExtn.value) {
|
if (dataVizExtn.value) {
|
||||||
@ -74,28 +91,25 @@ export default function useForgeSprite() {
|
|||||||
let spriteColor = new THREE.Color(0xffffff);
|
let spriteColor = new THREE.Color(0xffffff);
|
||||||
const BASEURL = import.meta.env.VITE_FILE_API_BASEURL;
|
const BASEURL = import.meta.env.VITE_FILE_API_BASEURL;
|
||||||
const spriteIconUrl = `${BASEURL}/dist/hotspot.svg`;
|
const spriteIconUrl = `${BASEURL}/dist/hotspot.svg`;
|
||||||
const style = new DataVizCore.ViewableStyle(
|
|
||||||
viewableType,
|
|
||||||
spriteColor,
|
|
||||||
spriteIconUrl
|
|
||||||
);
|
|
||||||
const viewableData = new DataVizCore.ViewableData();
|
const viewableData = new DataVizCore.ViewableData();
|
||||||
viewableData.spriteSize = 24; // Sprites as points of size 24 x 24 pixels
|
viewableData.spriteSize = 24; // Sprites as points of size 24 x 24 pixels
|
||||||
flatSubData.value?.forEach((d, index) => {
|
flatSubData.value?.forEach((d, index) => {
|
||||||
if (d.device_coordinate_3d) {
|
if (d.device_coordinate_3d) {
|
||||||
const position = d.device_coordinate_3d;
|
const position = d.device_coordinate_3d;
|
||||||
style.color = new THREE.Color(hexToRgb(d.device_normal_color));
|
// 每個都 new 一個 style
|
||||||
|
const pointStyle = new DataVizCore.ViewableStyle(
|
||||||
|
viewableType,
|
||||||
|
new THREE.Color(hexToRgb(getDeviceRealtimeColor(d))),
|
||||||
|
spriteIconUrl
|
||||||
|
);
|
||||||
const viewable = new DataVizCore.SpriteViewable(
|
const viewable = new DataVizCore.SpriteViewable(
|
||||||
position,
|
position,
|
||||||
style,
|
pointStyle,
|
||||||
d.spriteDbId
|
d.spriteDbId
|
||||||
);
|
);
|
||||||
viewableData.addViewable(viewable);
|
viewableData.addViewable(viewable);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
// await viewableData.finish();
|
|
||||||
// dataVizExtn.value.addViewables(viewableData);
|
|
||||||
// console.log(dataVizExtn.value);
|
|
||||||
viewableData.finish().then(
|
viewableData.finish().then(
|
||||||
() => {
|
() => {
|
||||||
dataVizExtn.value.addViewables(viewableData);
|
dataVizExtn.value.addViewables(viewableData);
|
||||||
@ -107,6 +121,16 @@ export default function useForgeSprite() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
// 監聽 realtimeData 變化,重建 sprites
|
||||||
|
watch(
|
||||||
|
() => realtimeData?.value,
|
||||||
|
() => {
|
||||||
|
if (forgeViewer.value?.isLoadDone()) {
|
||||||
|
createSprites();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ deep: true }
|
||||||
|
);
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => flatSubData,
|
() => flatSubData,
|
||||||
|
@ -18,7 +18,7 @@ const { items, changeActiveBtn, setItems, selectedBtn } = useActiveBtn();
|
|||||||
let intervalId = null;
|
let intervalId = null;
|
||||||
const energyCostData = ref({});
|
const energyCostData = ref({});
|
||||||
const FILE_BASEURL = import.meta.env.VITE_FILE_API_BASEURL;
|
const FILE_BASEURL = import.meta.env.VITE_FILE_API_BASEURL;
|
||||||
const imgBaseUrl = ref('');
|
const imgBaseUrl = ref("");
|
||||||
const formState = ref({
|
const formState = ref({
|
||||||
building_guid: null,
|
building_guid: null,
|
||||||
floor_guid: "all",
|
floor_guid: "all",
|
||||||
@ -35,8 +35,10 @@ watch(
|
|||||||
(newBuilding) => {
|
(newBuilding) => {
|
||||||
if (newBuilding) {
|
if (newBuilding) {
|
||||||
formState.value.building_guid = newBuilding.building_guid;
|
formState.value.building_guid = newBuilding.building_guid;
|
||||||
imgBaseUrl.value = `${FILE_BASEURL}/upload/setting/previewImage/${newBuilding.building_guid}${store.previewImageExt}`;
|
imgBaseUrl.value = store.previewImageExt
|
||||||
}
|
? `${FILE_BASEURL}/upload/setting/previewImage/${newBuilding.building_guid}${store.previewImageExt}`
|
||||||
|
: "";
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{ immediate: true, deep: true }
|
{ immediate: true, deep: true }
|
||||||
);
|
);
|
||||||
@ -119,7 +121,7 @@ onUnmounted(() => {
|
|||||||
)
|
)
|
||||||
"
|
"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Forge
|
<Forge
|
||||||
:class="
|
:class="
|
||||||
twMerge(
|
twMerge(
|
||||||
|
@ -3,6 +3,7 @@ import { onMounted, ref, inject, computed } from "vue";
|
|||||||
import { useI18n } from "vue-i18n";
|
import { useI18n } from "vue-i18n";
|
||||||
import { posttDashboard2D3D } from "@/apis/dashboard";
|
import { posttDashboard2D3D } from "@/apis/dashboard";
|
||||||
import useBuildingStore from "@/stores/useBuildingStore";
|
import useBuildingStore from "@/stores/useBuildingStore";
|
||||||
|
import { tr } from "date-fns/locale";
|
||||||
|
|
||||||
const { openToast, cancelToastOpen } = inject("app_toast");
|
const { openToast, cancelToastOpen } = inject("app_toast");
|
||||||
const buildingStore = useBuildingStore();
|
const buildingStore = useBuildingStore();
|
||||||
@ -38,6 +39,8 @@ const onOk = async () => {
|
|||||||
formData.append("is3DEnabled", formState.value.showForgeArea);
|
formData.append("is3DEnabled", formState.value.showForgeArea);
|
||||||
if (formState.value.lorf && formState.value.lorf.length > 0) {
|
if (formState.value.lorf && formState.value.lorf.length > 0) {
|
||||||
formData.append("file", formState.value.lorf[0]);
|
formData.append("file", formState.value.lorf[0]);
|
||||||
|
}else {
|
||||||
|
formData.append("removePreviewImage", true);
|
||||||
}
|
}
|
||||||
const res = await posttDashboard2D3D(formData);
|
const res = await posttDashboard2D3D(formData);
|
||||||
if (res.isSuccess) {
|
if (res.isSuccess) {
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { RouterView, useRoute } from "vue-router";
|
import { RouterView, useRoute } from "vue-router";
|
||||||
import { computed, watch, provide, ref, onMounted, onBeforeUnmount } from "vue";
|
import { computed, watch, provide, ref, onMounted, onUnmounted } from "vue";
|
||||||
import SystemFloorBar from "./components/SystemFloorBar.vue";
|
import SystemFloorBar from "./components/SystemFloorBar.vue";
|
||||||
import SystemDeptBar from "./components/SystemDeptBar.vue";
|
import SystemDeptBar from "./components/SystemDeptBar.vue";
|
||||||
import useBuildingStore from "@/stores/useBuildingStore";
|
import useBuildingStore from "@/stores/useBuildingStore";
|
||||||
@ -42,7 +42,7 @@ const floors = ref([]);
|
|||||||
const deptData = ref([]);
|
const deptData = ref([]);
|
||||||
const companyOptions = ref([]);
|
const companyOptions = ref([]);
|
||||||
const selected_dbid = ref([]);
|
const selected_dbid = ref([]);
|
||||||
const imgBaseUrl = ref('');
|
const imgBaseUrl = ref("");
|
||||||
|
|
||||||
const getFloors = async () => {
|
const getFloors = async () => {
|
||||||
const res = await getAssetFloorList();
|
const res = await getAssetFloorList();
|
||||||
@ -130,7 +130,9 @@ watch(
|
|||||||
(newBuilding) => {
|
(newBuilding) => {
|
||||||
if (Boolean(newBuilding)) {
|
if (Boolean(newBuilding)) {
|
||||||
getData();
|
getData();
|
||||||
imgBaseUrl.value = `${FILE_BASEURL}/upload/setting/previewImage/${newBuilding.building_guid}${buildingStore.previewImageExt}`;
|
imgBaseUrl.value = buildingStore.previewImageExt
|
||||||
|
? `${FILE_BASEURL}/upload/setting/previewImage/${newBuilding.building_guid}${buildingStore.previewImageExt}`
|
||||||
|
: "";
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -173,7 +175,7 @@ const updateCurrentFloor = (floor) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const realtimeData = ref([]);
|
const realtimeData = ref([]);
|
||||||
const timeId = ref(null);
|
let timeId = null;
|
||||||
const getAllDeviceRealtime = async () => {
|
const getAllDeviceRealtime = async () => {
|
||||||
// 立即執行一次
|
// 立即執行一次
|
||||||
const fetchData = async () => {
|
const fetchData = async () => {
|
||||||
@ -184,15 +186,22 @@ const getAllDeviceRealtime = async () => {
|
|||||||
realtimeData.value = res.data;
|
realtimeData.value = res.data;
|
||||||
};
|
};
|
||||||
await fetchData(); // 立即執行一次
|
await fetchData(); // 立即執行一次
|
||||||
|
if (timeId) {
|
||||||
|
clearInterval(timeId);
|
||||||
|
timeId = null;
|
||||||
|
}
|
||||||
// 然後設定每 10 秒更新一次
|
// 然後設定每 10 秒更新一次
|
||||||
timeId.value = setInterval(fetchData, 10000);
|
timeId = setInterval(fetchData, 10 * 1000);
|
||||||
};
|
};
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
subscribeData,
|
subscribeData,
|
||||||
(newValue) => {
|
(newValue) => {
|
||||||
timeId.value && clearInterval(timeId.value);
|
console.log("subscribeData changed:", newValue);
|
||||||
|
|
||||||
|
if (timeId) {
|
||||||
|
clearInterval(timeId);
|
||||||
|
}
|
||||||
newValue.length > 0 && getAllDeviceRealtime();
|
newValue.length > 0 && getAllDeviceRealtime();
|
||||||
},
|
},
|
||||||
{ deep: true, immediate: true }
|
{ deep: true, immediate: true }
|
||||||
@ -273,8 +282,11 @@ provide("system_selectedDevice", {
|
|||||||
deptData,
|
deptData,
|
||||||
});
|
});
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onUnmounted(() => {
|
||||||
clearInterval(timeId.value);
|
if (timeId) {
|
||||||
|
clearInterval(timeId);
|
||||||
|
timeId = null;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -1,13 +1,13 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { useRoute } from "vue-router";
|
import { useRoute } from "vue-router";
|
||||||
import EffectScatter from "@/components/chart/EffectScatter.vue";
|
import EffectScatter from "@/components/chart/EffectScatter.vue";
|
||||||
import { computed, inject, ref, watch } from "vue";
|
import { computed, inject, nextTick, ref, watch } from "vue";
|
||||||
import { twMerge } from "tailwind-merge";
|
import { twMerge } from "tailwind-merge";
|
||||||
import useSelectedFloor from "@/hooks/useSelectedFloor";
|
import useSelectedFloor from "@/hooks/useSelectedFloor";
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
|
|
||||||
const { currentFloor, subscribeData } = inject("system_deviceList");
|
const { currentFloor, subscribeData, realtimeData } = inject("system_deviceList");
|
||||||
const { getCurrentInfoModalData, selected_dbid } = inject(
|
const { getCurrentInfoModalData, selected_dbid } = inject(
|
||||||
"system_selectedDevice"
|
"system_selectedDevice"
|
||||||
);
|
);
|
||||||
@ -22,6 +22,19 @@ const sameOption = {
|
|||||||
tooltip: 2,
|
tooltip: 2,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 根據設備取得即時狀態顏色
|
||||||
|
const getDeviceRealtimeColor = (device) => {
|
||||||
|
if (device.full_name === 'SmartSocket-AA001') return 'red';
|
||||||
|
if (device.full_name === 'SmartSocket-AA003' || device.full_name === 'SmartSocket-AA004') return 'gray';
|
||||||
|
const realtimeDevice = realtimeData.value?.find(
|
||||||
|
(item) => item.device_number === device.device_number
|
||||||
|
);
|
||||||
|
const state = realtimeDevice?.state || '';
|
||||||
|
if (state === 'offnormal' || state === '') return device.device_close_color || '#999';
|
||||||
|
return device.device_normal_color || '#009100';
|
||||||
|
};
|
||||||
|
|
||||||
const defaultOption = (map, data = []) => {
|
const defaultOption = (map, data = []) => {
|
||||||
return {
|
return {
|
||||||
animation: false,
|
animation: false,
|
||||||
@ -38,13 +51,7 @@ const defaultOption = (map, data = []) => {
|
|||||||
...sameOption,
|
...sameOption,
|
||||||
symbolSize: 10,
|
symbolSize: 10,
|
||||||
itemStyle: {
|
itemStyle: {
|
||||||
color: (params) =>
|
color: (params) => getDeviceRealtimeColor(params.data[2]),
|
||||||
params.data[2].full_name === "SmartSocket-AA001"
|
|
||||||
? "red"
|
|
||||||
: params.data[2].full_name === "SmartSocket-AA003" ||
|
|
||||||
params.data[2].full_name === "SmartSocket-AA004"
|
|
||||||
? "gray"
|
|
||||||
: params.data[2].device_normal_color || "#009100",
|
|
||||||
},
|
},
|
||||||
data,
|
data,
|
||||||
},
|
},
|
||||||
@ -52,19 +59,32 @@ const defaultOption = (map, data = []) => {
|
|||||||
...sameOption,
|
...sameOption,
|
||||||
symbolSize: 20,
|
symbolSize: 20,
|
||||||
itemStyle: {
|
itemStyle: {
|
||||||
color: (params) =>
|
color: (params) => getDeviceRealtimeColor(params.data[2]),
|
||||||
params.data[2].full_name === "SmartSocket-AA001"
|
|
||||||
? "red"
|
|
||||||
: params.data[2].full_name === "SmartSocket-AA003" ||
|
|
||||||
params.data[2].full_name === "SmartSocket-AA004"
|
|
||||||
? "gray"
|
|
||||||
: params.data[2].device_normal_color || "#009100",
|
|
||||||
},
|
},
|
||||||
data: [],
|
data: [],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
// 監聽 realtimeData 變化,刷新地圖顏色
|
||||||
|
watch(
|
||||||
|
realtimeData,
|
||||||
|
() => {
|
||||||
|
nextTick(() => {
|
||||||
|
if (
|
||||||
|
selectedFloor.value &&
|
||||||
|
asset_floor_chart.value &&
|
||||||
|
asset_floor_chart.value.chart
|
||||||
|
) {
|
||||||
|
asset_floor_chart.value.chart.setOption(
|
||||||
|
defaultOption(selectedFloor.value?.title, selectedData.value),
|
||||||
|
true
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
{ deep: true }
|
||||||
|
);
|
||||||
|
|
||||||
const { selectedFloor } = useSelectedFloor();
|
const { selectedFloor } = useSelectedFloor();
|
||||||
|
|
||||||
|
@ -5,11 +5,40 @@ import useSystemShowData from "@/hooks/useSystemShowData";
|
|||||||
const { getCurrentInfoModalData, selected_dbid } = inject(
|
const { getCurrentInfoModalData, selected_dbid } = inject(
|
||||||
"system_selectedDevice"
|
"system_selectedDevice"
|
||||||
);
|
);
|
||||||
const { subscribeData } = inject("system_deviceList");
|
const { subscribeData, realtimeData } = inject("system_deviceList");
|
||||||
|
|
||||||
const { showData } = useSystemShowData();
|
const { showData } = useSystemShowData();
|
||||||
|
|
||||||
const FILE_BASEURL = import.meta.env.VITE_FILE_API_BASEURL;
|
const FILE_BASEURL = import.meta.env.VITE_FILE_API_BASEURL;
|
||||||
|
|
||||||
|
|
||||||
|
// 根據設備編號取得即時狀態
|
||||||
|
const getDeviceRealtimeState = (deviceNumber) => {
|
||||||
|
const realtimeDevice = realtimeData.value?.find(
|
||||||
|
(item) => item.device_number === deviceNumber
|
||||||
|
);
|
||||||
|
return realtimeDevice?.state || '';
|
||||||
|
};
|
||||||
|
|
||||||
|
// 狀態顏色
|
||||||
|
const getDeviceStatusColor = (device) => {
|
||||||
|
if (device.full_name === 'SmartSocket-AA001') return 'red';
|
||||||
|
if (device.full_name === 'SmartSocket-AA003' || device.full_name === 'SmartSocket-AA004') return 'gray';
|
||||||
|
const state = getDeviceRealtimeState(device.device_number);
|
||||||
|
if (state === 'offnormal' || state === '') return device.device_close_color || 'gray';
|
||||||
|
return device.device_normal_color;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 狀態文字
|
||||||
|
const getDeviceStatusText = (device) => {
|
||||||
|
if (device.full_name === 'SmartSocket-AA001') return 'Error';
|
||||||
|
if (device.full_name === 'SmartSocket-AA003' || device.full_name === 'SmartSocket-AA004') return 'Offline';
|
||||||
|
const state = getDeviceRealtimeState(device.device_number);
|
||||||
|
if (state === 'offnormal' || state === '') return 'Offline';
|
||||||
|
if (state === 'normal') return 'Online';
|
||||||
|
return state || device.device_status || 'Online';
|
||||||
|
};
|
||||||
|
|
||||||
const fitToView = (forge_dbid, spriteDbId) => {
|
const fitToView = (forge_dbid, spriteDbId) => {
|
||||||
selected_dbid.value = [forge_dbid, spriteDbId];
|
selected_dbid.value = [forge_dbid, spriteDbId];
|
||||||
};
|
};
|
||||||
@ -72,25 +101,10 @@ const cancelDialog = () => {
|
|||||||
<div class="sec03">
|
<div class="sec03">
|
||||||
<span
|
<span
|
||||||
class="w-5 h-5 rounded-full"
|
class="w-5 h-5 rounded-full"
|
||||||
:style="{
|
:style="{ backgroundColor: getDeviceStatusColor(device) }"
|
||||||
backgroundColor:
|
|
||||||
device.full_name === 'SmartSocket-AA001'
|
|
||||||
? 'red'
|
|
||||||
: device.full_name === 'SmartSocket-AA003' ||
|
|
||||||
device.full_name === 'SmartSocket-AA004'
|
|
||||||
? 'gray'
|
|
||||||
: device.device_normal_color,
|
|
||||||
}"
|
|
||||||
></span>
|
></span>
|
||||||
<span class="mx-2">{{ $t("system.status") }}:</span>
|
<span class="mx-2">{{ $t("system.status") }}:</span>
|
||||||
<span>{{
|
<span>{{ getDeviceStatusText(device) }}</span>
|
||||||
device.full_name === "SmartSocket-AA001"
|
|
||||||
? "Error"
|
|
||||||
: device.full_name === "SmartSocket-AA003" ||
|
|
||||||
device.full_name === "SmartSocket-AA004"
|
|
||||||
? "Offline"
|
|
||||||
: device.device_status || 'Online'
|
|
||||||
}}</span>
|
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
class="btn-text border-0"
|
class="btn-text border-0"
|
||||||
|
Loading…
Reference in New Issue
Block a user