95 lines
2.7 KiB
Vue
95 lines
2.7 KiB
Vue
<script setup>
|
|
import { ref, onMounted } from 'vue'
|
|
import { inject } from 'vue'
|
|
|
|
const mqttRef = inject('mqtt')
|
|
const emit = defineEmits(['select'])
|
|
|
|
const devices = ref([])
|
|
const deviceMap = ref({}) // map deviceId => device object with all meta properties
|
|
const selected = ref(null)
|
|
|
|
// Add or update a device's meta property
|
|
function upsertDevice(deviceId, property, value) {
|
|
if (!deviceMap.value[deviceId]) {
|
|
deviceMap.value[deviceId] = {
|
|
id: deviceId,
|
|
meta: {}, // stores all meta properties
|
|
title: deviceId,
|
|
value: '', // main status display
|
|
subtitle: 'MQTT Device',
|
|
icon: 'pi-box',
|
|
}
|
|
}
|
|
|
|
// update the property
|
|
deviceMap.value[deviceId].meta[property] = value
|
|
|
|
// update card display values (example: show 'status' as main value)
|
|
if (property === 'status') {
|
|
deviceMap.value[deviceId].value = value.replace(/['"]+/g, '')
|
|
deviceMap.value[deviceId].icon = deviceMap.value[deviceId].value === 'ONLINE' ? 'pi-check-circle' : 'pi-times-circle'
|
|
}
|
|
|
|
// rebuild reactive array for rendering
|
|
devices.value = Object.values(deviceMap.value)
|
|
}
|
|
|
|
function selectDevice(device) {
|
|
selected.value = device.id
|
|
emit('select', device.id)
|
|
}
|
|
|
|
onMounted(() => {
|
|
mqttRef.value.subscribe('device/+/meta/+', (payload, topic) => {
|
|
const parts = topic.split('/')
|
|
const deviceId = parts[1]
|
|
const property = parts.slice(3).join('/') // handles nested properties if any
|
|
upsertDevice(deviceId, property, payload)
|
|
})
|
|
})
|
|
</script>
|
|
|
|
<template>
|
|
<div class="layout-grid-row">
|
|
<div
|
|
v-for="device in devices"
|
|
:key="device.id"
|
|
class="layout-card device-card"
|
|
:class="{ 'selected-device': device.id === selected }"
|
|
@click="selectDevice(device)"
|
|
>
|
|
<div class="stats-header">
|
|
<span class="stats-title">{{ device.meta['type'] }}</span>
|
|
<span :class="[ device.value === 'OFFLINE' ? 'offline' : '', 'stats-icon-box']">
|
|
<i
|
|
:class="['pi', device.icon]"
|
|
:style="{ color: device.value === 'ONLINE' ? 'inherit' : 'var(--p-secondary-color)' }"
|
|
></i>
|
|
</span>
|
|
</div>
|
|
|
|
<div class="stats-content">
|
|
<div class="stats-value">{{ device.meta['name'] }}</div>
|
|
<div class="stats-subtitle"><span class='italic'>{{ device.id }}</span> on <span class="font-bold">{{ device.meta['host'] }}</span></div>
|
|
<div class="stats-subtitle italic text-green-500"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.device-card {
|
|
cursor: pointer;
|
|
transition: all 0.2s ease;
|
|
}
|
|
|
|
.device-card:hover {
|
|
transform: translateY(-2px);
|
|
}
|
|
|
|
.selected-device {
|
|
border: 2px solid var(--p-primary-color);
|
|
box-shadow: 0 0 8px var(--p-primary-color);
|
|
}
|
|
</style> |