general dashboard
This commit is contained in:
111
src/components/cards/GraphMetricCard.vue
Normal file
111
src/components/cards/GraphMetricCard.vue
Normal 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>
|
||||
18
src/components/cards/MetricCard.vue
Normal file
18
src/components/cards/MetricCard.vue
Normal 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>
|
||||
79
src/components/cards/RealtimeEnergyChartCard.vue
Normal file
79
src/components/cards/RealtimeEnergyChartCard.vue
Normal 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>
|
||||
95
src/components/cards/SensorConsumptionTable.vue
Normal file
95
src/components/cards/SensorConsumptionTable.vue
Normal 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>
|
||||
Reference in New Issue
Block a user