general dashboard

This commit is contained in:
rafaeldpsilva
2025-09-02 14:19:05 +01:00
commit 0db018f939
41 changed files with 9025 additions and 0 deletions

View File

@@ -0,0 +1,111 @@
<template>
<div class="bg-white rounded-2xl shadow-sm flex flex-col aspect-square p-4">
<h6 class="text-sm font-bold text-gray-500 mb-2">{{ title }}</h6>
<div class="flex-grow flex items-center">
<p class="text-gray-900 font-bold text-2xl">
{{ content }} <span class="text-sm text-gray-500">{{ details }}</span>
</p>
</div>
<div class="h-16 mt-2">
<v-chart class="w-full h-full" :option="chartOption" :autoresize="true" />
</div>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { use } from 'echarts/core'
import { CanvasRenderer } from 'echarts/renderers'
import { LineChart } from 'echarts/charts'
import { GridComponent } from 'echarts/components'
import * as echarts from 'echarts/core'
use([LineChart, CanvasRenderer, GridComponent])
const props = defineProps<{
title: string
content: string | number
details?: string | number
trendData?: number[]
trendDirection?: 'up' | 'down' | 'neutral'
}>()
// Default trend data if none provided
const defaultTrendData = [20, 25, 18, 30, 28, 35, 25, 40]
const trendData = computed(() => props.trendData || defaultTrendData)
const trendDir = computed(() => {
const dir = trendData.value[trendData.value.length - 1] - trendData.value[0]
console.log(dir)
if (dir > 0) return 'up'
if (dir < 0) return 'down'
return 'neutral'
})
// Determine trend color based on direction
const trendColor = computed(() => {
switch (trendDir.value) {
case 'up':
return '#22c55e' // green
case 'down':
return '#ef4444' // red
default:
return '#3b82f6' // blue
}
})
const areaColor = computed(() => {
switch (trendDir.value) {
case 'up':
return '#7ee6a4' // green
case 'down':
return '#f07373' // red
default:
return '#619dff' // blue
}
})
// ECharts configuration for mini trend chart
const chartOption = computed(() => ({
textStyle: {
fontFamily:
'ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"',
},
grid: {
left: 0,
right: 0,
top: 0,
bottom: 0,
},
xAxis: {
type: 'category',
show: false,
data: trendData.value.map((_, index) => index),
},
yAxis: {
type: 'value',
show: false,
},
series: [
{
data: trendData.value,
type: 'line',
smooth: true,
showSymbol: false,
lineStyle: {
color: trendColor.value,
width: 2,
},
areaStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 0.7, [
{
offset: 0,
color: areaColor.value,
},
{
offset: 1,
color: 'rgb(255, 255, 255)',
},
]),
},
animation: false, // Disable animation for better performance in small charts
},
],
}))
</script>

View File

@@ -0,0 +1,18 @@
<template>
<div class="bg-white rounded-2xl shadow-sm flex flex-col justify-between aspect-square p-4">
<h6 class="text-sm font-bold text-gray-500">{{ title }}</h6>
<div class="flex-grow flex items-center justify-start">
<p class="text-gray-900 font-bold text-2xl">
{{ content }} <span class="text-sm text-gray-500">{{ details }}</span>
</p>
</div>
</div>
</template>
<script setup lang="ts">
defineProps<{
title: string
content: string | number
details?: string | number
}>()
</script>

View File

@@ -0,0 +1,79 @@
<template>
<div class="bg-white rounded-2xl shadow-sm flex flex-col h-full min-h-[300px]">
<div class="p-4 h-full">
<h6 class="text-sm font-bold text-gray-500 mb-2">{{ title }}</h6>
<v-chart class="h-64 w-full" :option="option" autoresize />
</div>
</div>
</template>
<script setup lang="ts">
import { use } from 'echarts/core'
import { CanvasRenderer } from 'echarts/renderers'
import { LineChart } from 'echarts/charts'
import {
TitleComponent,
TooltipComponent,
LegendComponent,
GridComponent,
} from 'echarts/components'
import { computed } from 'vue'
import { useEnergyStore } from '@/stores/energy'
use([TitleComponent, TooltipComponent, LegendComponent, LineChart, CanvasRenderer, GridComponent])
defineProps<{
title: string
}>()
const energyStore = useEnergyStore()
const option = computed(() => ({
grid: {
left: 35,
right: 30,
top: 40,
bottom: 40,
},
textStyle: {
fontFamily:
'ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"',
},
tooltip: {
trigger: 'axis',
textStyle: {
fontFamily:
'ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"',
},
},
xAxis: {
type: 'category',
data: energyStore.timeSeriesData.labels,
axisLabel: {
fontFamily:
'ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"',
},
},
yAxis: {
type: 'value',
axisLabel: {
fontFamily:
'ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"',
},
},
series: [
{
data: energyStore.timeSeriesData.datasets[0].data,
type: 'line',
showSymbol: false,
smooth: true,
itemStyle: {
color: '#3b82f6'
},
lineStyle: {
color: '#3b82f6'
}
},
],
}))
</script>

View File

@@ -0,0 +1,95 @@
<template>
<div class="bg-white rounded-2xl shadow-sm p-4">
<h6 class="text-sm font-bold text-gray-500 mb-4">Sensor Consumption</h6>
<div class="overflow-x-auto">
<table class="min-w-full">
<thead>
<tr class="border-b border-gray-200">
<th class="text-left text-xs font-medium text-gray-500 uppercase tracking-wider py-3">
Sensor ID
</th>
<th class="text-right text-xs font-medium text-gray-500 uppercase tracking-wider py-3">
Current
</th>
<th class="text-right text-xs font-medium text-gray-500 uppercase tracking-wider py-3">
Total
</th>
<th class="text-right text-xs font-medium text-gray-500 uppercase tracking-wider py-3">
Average
</th>
<th class="text-right text-xs font-medium text-gray-500 uppercase tracking-wider py-3">
Last Updated
</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-100">
<tr v-if="sensorList.length === 0" class="py-4">
<td colspan="5" class="text-center text-gray-500 py-8">
No sensor data available. Waiting for WebSocket connection...
</td>
</tr>
<tr v-for="sensor in sensorList" :key="sensor.sensorId" class="hover:bg-gray-50">
<td class="py-3 text-sm font-medium text-gray-900">
{{ sensor.sensorId }}
</td>
<td class="py-3 text-sm text-gray-600 text-right">
{{ sensor.latestValue.toFixed(2) }} {{ sensor.unit }}
</td>
<td class="py-3 text-sm text-gray-600 text-right">
{{ sensor.totalConsumption.toFixed(2) }} {{ sensor.unit }}
</td>
<td class="py-3 text-sm text-gray-600 text-right">
{{ sensor.averageConsumption.toFixed(2) }} {{ sensor.unit }}
</td>
<td class="py-3 text-sm text-gray-500 text-right">
{{ formatTime(sensor.lastUpdated) }}
</td>
</tr>
</tbody>
</table>
</div>
<!-- Connection Status Indicator -->
<div class="mt-4 flex items-center justify-between text-xs text-gray-500">
<div class="flex items-center gap-2">
<div
class="w-2 h-2 rounded-full"
:class="energyStore.isConnected ? 'bg-green-500' : 'bg-red-500'"
></div>
<span>{{ energyStore.isConnected ? 'Connected' : 'Disconnected' }}</span>
</div>
<div>
{{ sensorList.length }} sensor{{ sensorList.length !== 1 ? 's' : '' }} active
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { useEnergyStore } from '@/stores/energy'
const energyStore = useEnergyStore()
const sensorList = computed(() => {
return Array.from(energyStore.sensorsData.values()).sort((a, b) =>
a.sensorId.localeCompare(b.sensorId)
)
})
const formatTime = (timestamp: number) => {
const date = new Date(timestamp * 1000)
const now = new Date()
const diffMs = now.getTime() - date.getTime()
const diffSecs = Math.floor(diffMs / 1000)
if (diffSecs < 60) {
return `${diffSecs}s ago`
} else if (diffSecs < 3600) {
return `${Math.floor(diffSecs / 60)}m ago`
} else {
return date.toLocaleTimeString()
}
}
</script>