This commit is contained in:
2026-02-02 11:00:40 +03:00
parent 87a1a628d3
commit 2d0f236fa4
22 changed files with 1119 additions and 461 deletions

View File

@@ -15,44 +15,182 @@ interface AreaChartProps {
const AreaChart: React.FC<AreaChartProps> = ({ className = '', data }) => {
const width = 635
const height = 200
const paddingBottom = 20
const baselineY = height - paddingBottom
const maxPlotHeight = height - 40
const height = 280
const margin = { top: 20, right: 30, bottom: 50, left: 60 }
const plotWidth = width - margin.left - margin.right
const plotHeight = height - margin.top - margin.bottom
const baselineY = margin.top + plotHeight
const safeData = (Array.isArray(data) && data.length > 0)
? data
: Array.from({ length: 7 }, () => ({ value: 0 }))
const maxVal = Math.max(...safeData.map(d => d.value || 0), 1)
const stepX = safeData.length > 1 ? width / (safeData.length - 1) : width
const stepX = safeData.length > 1 ? plotWidth / (safeData.length - 1) : plotWidth
const points = safeData.map((d, i) => {
const x = i * stepX
const y = baselineY - (Math.min(d.value || 0, maxVal) / maxVal) * maxPlotHeight
const x = margin.left + i * stepX
const y = baselineY - (Math.min(d.value || 0, maxVal) / maxVal) * plotHeight
return { x, y }
})
const linePath = points.map((p, i) => `${i === 0 ? 'M' : 'L'}${p.x},${p.y}`).join(' ')
const areaPath = `${linePath} L${width},${baselineY} L0,${baselineY} Z`
const areaPath = `${linePath} L${width - margin.right},${baselineY} L${margin.left},${baselineY} Z`
// Генерируем Y-оси метки
const ySteps = 4
const yLabels = Array.from({ length: ySteps + 1 }, (_, i) => {
const value = (maxVal / ySteps) * (ySteps - i)
const y = margin.top + (i * plotHeight) / ySteps
return { value: value.toFixed(1), y }
})
// Генерируем X-оси метки (показываем каждую 2-ю или 3-ю точку)
const xLabelStep = Math.ceil(safeData.length / 5)
const xLabels = safeData
.map((d, i) => {
const x = margin.left + i * stepX
const label = d.label || d.timestamp || `${i + 1}`
return { label, x, index: i }
})
.filter((_, i) => i % xLabelStep === 0 || i === safeData.length - 1)
return (
<div className={`w-full h-full ${className}`}>
<svg className="w-full h-full" viewBox={`0 0 ${width} ${height}`}>
<defs>
<linearGradient id="areaGradient" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor="rgb(42, 157, 144)" stopOpacity="0.3" />
<stop offset="100%" stopColor="rgb(42, 157, 144)" stopOpacity="0" />
<stop offset="0%" stopColor="rgb(37, 99, 235)" stopOpacity="0.3" />
<stop offset="100%" stopColor="rgb(37, 99, 235)" stopOpacity="0" />
</linearGradient>
</defs>
<path d={areaPath} fill="url(#areaGradient)" />
<path d={linePath} stroke="rgb(42, 157, 144)" strokeWidth="2" fill="none" />
</defs>
{/* Сетка Y */}
{yLabels.map((label, i) => (
<line
key={`grid-y-${i}`}
x1={margin.left}
y1={label.y}
x2={width - margin.right}
y2={label.y}
stroke="rgba(148, 163, 184, 0.2)"
strokeWidth="1"
strokeDasharray="4,4"
/>
))}
{/* Ось X */}
<line
x1={margin.left}
y1={baselineY}
x2={width - margin.right}
y2={baselineY}
stroke="rgb(148, 163, 184)"
strokeWidth="2"
/>
{/* Ось Y */}
<line
x1={margin.left}
y1={margin.top}
x2={margin.left}
y2={baselineY}
stroke="rgb(148, 163, 184)"
strokeWidth="2"
/>
{/* Y-оси метки и подписи */}
{yLabels.map((label, i) => (
<g key={`y-label-${i}`}>
<line
x1={margin.left - 5}
y1={label.y}
x2={margin.left}
y2={label.y}
stroke="rgb(148, 163, 184)"
strokeWidth="1"
/>
<text
x={margin.left - 10}
y={label.y + 4}
textAnchor="end"
fontSize="12"
fill="rgb(148, 163, 184)"
fontFamily="Arial, sans-serif"
>
{label.value}
</text>
</g>
))}
{/* X-оси метки и подписи */}
{xLabels.map((label, i) => (
<g key={`x-label-${i}`}>
<line
x1={label.x}
y1={baselineY}
x2={label.x}
y2={baselineY + 5}
stroke="rgb(148, 163, 184)"
strokeWidth="1"
/>
<text
x={label.x}
y={baselineY + 20}
textAnchor="middle"
fontSize="11"
fill="rgb(148, 163, 184)"
fontFamily="Arial, sans-serif"
>
{typeof label.label === 'string' ? label.label.substring(0, 10) : `${label.index + 1}`}
</text>
</g>
))}
{/* Подпись оси Y */}
<text
x={20}
y={margin.top + plotHeight / 2}
textAnchor="middle"
fontSize="13"
fill="rgb(148, 163, 184)"
fontFamily="Arial, sans-serif"
transform={`rotate(-90, 20, ${margin.top + plotHeight / 2})`}
>
Значение
</text>
{/* Подпись оси X */}
<text
x={margin.left + plotWidth / 2}
y={height - 10}
textAnchor="middle"
fontSize="13"
fill="rgb(148, 163, 184)"
fontFamily="Arial, sans-serif"
>
Время
</text>
{/* График */}
<path d={areaPath} fill="url(#areaGradient)" />
<path d={linePath} stroke="rgb(37, 99, 235)" strokeWidth="2.5" fill="none" />
{/* Точки данных */}
{points.map((p, i) => (
<circle key={i} cx={p.x} cy={p.y} r="3" fill="rgb(42, 157, 144)" />
<circle
key={i}
cx={p.x}
cy={p.y}
r="4"
fill="rgb(37, 99, 235)"
stroke="rgb(15, 23, 42)"
strokeWidth="2"
/>
))}
</svg>
</div>
)
}
export default AreaChart
export default AreaChart

View File

@@ -14,26 +14,159 @@ interface BarChartProps {
}
const BarChart: React.FC<BarChartProps> = ({ className = '', data }) => {
const width = 635
const height = 280
const margin = { top: 20, right: 30, bottom: 50, left: 60 }
const plotWidth = width - margin.left - margin.right
const plotHeight = height - margin.top - margin.bottom
const baselineY = margin.top + plotHeight
const barData = (Array.isArray(data) && data.length > 0)
? data.map(d => ({ value: d.value, color: d.color || 'rgb(42, 157, 144)' }))
: Array.from({ length: 12 }, () => ({ value: 0, color: 'rgb(42, 157, 144)' }))
? data.map(d => ({ value: d.value, label: d.label || '', color: d.color || 'rgb(37, 99, 235)' }))
: Array.from({ length: 12 }, (_, i) => ({ value: 0, label: `${i + 1}`, color: 'rgb(37, 99, 235)' }))
const maxVal = Math.max(...barData.map(b => b.value || 0), 1)
// Генерируем Y-оси метки
const ySteps = 4
const yLabels = Array.from({ length: ySteps + 1 }, (_, i) => {
const value = (maxVal / ySteps) * (ySteps - i)
const y = margin.top + (i * plotHeight) / ySteps
return { value: value.toFixed(1), y }
})
// Генерируем X-оси метки (показываем каждую 2-ю или 3-ю)
const xLabelStep = Math.ceil(barData.length / 8)
const xLabels = barData
.map((d, i) => {
const barWidth = Math.max(30, plotWidth / barData.length - 8)
const barSpacing = (plotWidth - barWidth * barData.length) / (barData.length - 1 || 1)
const x = margin.left + i * (barWidth + barSpacing) + barWidth / 2
return { label: d.label || `${i + 1}`, x, index: i }
})
.filter((_, i) => i % xLabelStep === 0 || i === barData.length - 1)
return (
<div className={`w-full h-full ${className}`}>
<svg className="w-full h-full" viewBox="0 0 635 200">
<g>
{barData.map((bar, index) => {
const barWidth = 40
const barSpacing = 12
const x = index * (barWidth + barSpacing) + 20
const barHeight = (bar.value / maxVal) * 160
const y = 180 - barHeight
return (
<svg className="w-full h-full" viewBox={`0 0 ${width} ${height}`}>
{/* Сетка Y */}
{yLabels.map((label, i) => (
<line
key={`grid-y-${i}`}
x1={margin.left}
y1={label.y}
x2={width - margin.right}
y2={label.y}
stroke="rgba(148, 163, 184, 0.2)"
strokeWidth="1"
strokeDasharray="4,4"
/>
))}
{/* Ось X */}
<line
x1={margin.left}
y1={baselineY}
x2={width - margin.right}
y2={baselineY}
stroke="rgb(148, 163, 184)"
strokeWidth="2"
/>
{/* Ось Y */}
<line
x1={margin.left}
y1={margin.top}
x2={margin.left}
y2={baselineY}
stroke="rgb(148, 163, 184)"
strokeWidth="2"
/>
{/* Y-оси метки и подписи */}
{yLabels.map((label, i) => (
<g key={`y-label-${i}`}>
<line
x1={margin.left - 5}
y1={label.y}
x2={margin.left}
y2={label.y}
stroke="rgb(148, 163, 184)"
strokeWidth="1"
/>
<text
x={margin.left - 10}
y={label.y + 4}
textAnchor="end"
fontSize="12"
fill="rgb(148, 163, 184)"
fontFamily="Arial, sans-serif"
>
{label.value}
</text>
</g>
))}
{/* X-оси метки и подписи */}
{xLabels.map((label, i) => (
<g key={`x-label-${i}`}>
<line
x1={label.x}
y1={baselineY}
x2={label.x}
y2={baselineY + 5}
stroke="rgb(148, 163, 184)"
strokeWidth="1"
/>
<text
x={label.x}
y={baselineY + 20}
textAnchor="middle"
fontSize="11"
fill="rgb(148, 163, 184)"
fontFamily="Arial, sans-serif"
>
{typeof label.label === 'string' ? label.label.substring(0, 8) : `${label.index + 1}`}
</text>
</g>
))}
{/* Подпись оси Y */}
<text
x={20}
y={margin.top + plotHeight / 2}
textAnchor="middle"
fontSize="13"
fill="rgb(148, 163, 184)"
fontFamily="Arial, sans-serif"
transform={`rotate(-90, 20, ${margin.top + plotHeight / 2})`}
>
Значение
</text>
{/* Подпись оси X */}
<text
x={margin.left + plotWidth / 2}
y={height - 10}
textAnchor="middle"
fontSize="13"
fill="rgb(148, 163, 184)"
fontFamily="Arial, sans-serif"
>
Период
</text>
{/* Столбцы */}
{barData.map((bar, index) => {
const barWidth = Math.max(30, plotWidth / barData.length - 8)
const barSpacing = (plotWidth - barWidth * barData.length) / (barData.length - 1 || 1)
const x = margin.left + index * (barWidth + barSpacing)
const barHeight = (bar.value / maxVal) * plotHeight
const y = baselineY - barHeight
return (
<g key={`bar-${index}`}>
<rect
key={index}
x={x}
y={y}
width={barWidth}
@@ -41,13 +174,27 @@ const BarChart: React.FC<BarChartProps> = ({ className = '', data }) => {
fill={bar.color}
rx="4"
ry="4"
opacity="0.9"
/>
)
})}
</g>
{/* Тень для глубины */}
<rect
x={x}
y={y}
width={barWidth}
height={barHeight}
fill="none"
stroke={bar.color}
strokeWidth="1"
rx="4"
ry="4"
opacity="0.3"
/>
</g>
)
})}
</svg>
</div>
)
}
export default BarChart
export default BarChart

View File

@@ -15,13 +15,16 @@ const ChartCard: React.FC<ChartCardProps> = ({
children,
className = ''
}) => {
const interSemiboldStyle = { fontFamily: 'Inter, sans-serif', fontWeight: 600 }
const interRegularStyle = { fontFamily: 'Inter, sans-serif', fontWeight: 400 }
return (
<div className={`bg-[#161824] rounded-[20px] p-6 ${className}`}>
<div className="flex items-start justify-between mb-6">
<div>
<h3 className="text-white text-base font-semibold mb-1">{title}</h3>
<h3 style={interSemiboldStyle} className="text-white text-sm mb-1">{title}</h3>
{subtitle && (
<p className="text-[#71717a] text-sm">{subtitle}</p>
<p style={interRegularStyle} className="text-[#71717a] text-xs">{subtitle}</p>
)}
</div>
<div className="w-4 h-4">
@@ -38,4 +41,4 @@ const ChartCard: React.FC<ChartCardProps> = ({
)
}
export default ChartCard
export default ChartCard

View File

@@ -1,12 +1,14 @@
'use client'
import React, { useEffect, useState } from 'react'
import React, { useEffect, useState, useMemo } from 'react'
import { useRouter } from 'next/navigation'
import Sidebar from '../ui/Sidebar'
import AnimatedBackground from '../ui/AnimatedBackground'
import useNavigationStore from '../../app/store/navigationStore'
import ChartCard from './ChartCard'
import AreaChart from './AreaChart'
import BarChart from './BarChart'
import { aggregateChartDataByDays } from '../../lib/chartDataAggregator'
const Dashboard: React.FC = () => {
const router = useRouter()
@@ -14,7 +16,7 @@ const Dashboard: React.FC = () => {
const objectTitle = currentObject?.title
const [dashboardAlerts, setDashboardAlerts] = useState<any[]>([])
const [chartData, setChartData] = useState<{ timestamp: string; value: number }[]>([])
const [rawChartData, setRawChartData] = useState<{ timestamp: string; value: number }[]>([])
const [sensorTypes] = useState<Array<{code: string, name: string}>>([
{ code: '', name: 'Все датчики' },
{ code: 'GA', name: 'Инклинометр' },
@@ -52,7 +54,7 @@ const Dashboard: React.FC = () => {
setDashboardAlerts(tableData as any[])
const cd = Array.isArray(payload?.data?.chart_data) ? payload.data.chart_data : []
setChartData(cd as any[])
setRawChartData(cd as any[])
} catch (e) {
console.error('Failed to load dashboard:', e)
}
@@ -129,14 +131,25 @@ const Dashboard: React.FC = () => {
const handleTablePeriodChange = (period: string) => {
setSelectedTablePeriod(period)
}
// Агрегируем данные графика в зависимости от периода
const chartData = useMemo(() => {
return aggregateChartDataByDays(rawChartData, selectedChartPeriod)
}, [rawChartData, selectedChartPeriod])
const interSemiboldStyle = { fontFamily: 'Inter, sans-serif', fontWeight: 600 }
const interRegularStyle = { fontFamily: 'Inter, sans-serif', fontWeight: 400 }
return (
<div className="flex h-screen bg-[#0e111a]">
<Sidebar
activeItem={1} // Dashboard
/>
<div className="relative flex h-screen bg-[#0e111a] overflow-hidden">
<AnimatedBackground />
<div className="relative z-20">
<Sidebar
activeItem={1} // Dashboard
/>
</div>
<div className="flex-1 flex flex-col">
<div className="relative z-10 flex-1 flex flex-col">
<header className="bg-[#161824] border-b border-gray-700 px-6 py-4">
<div className="flex items-center gap-4">
<button
@@ -158,7 +171,7 @@ const Dashboard: React.FC = () => {
<div className="flex-1 p-6 overflow-auto">
<div className="mb-6">
<h1 className="text-white text-2xl font-semibold mb-6">{objectTitle || 'Объект'}</h1>
<h1 style={interSemiboldStyle} className="text-white text-2xl mb-6">{objectTitle || 'Объект'}</h1>
<div className="flex items-center gap-3 mb-6">
<div className="relative">
@@ -215,7 +228,7 @@ const Dashboard: React.FC = () => {
<ChartCard
title="Статистика"
>
<BarChart data={chartData?.map((d: any) => ({ value: d.value }))} />
<BarChart data={chartData?.map((d: any) => ({ value: d.value, label: d.label }))} />
</ChartCard>
</div>
</div>
@@ -224,7 +237,7 @@ const Dashboard: React.FC = () => {
<div>
<div>
<div className="flex items-center justify-between mb-6">
<h2 className="text-white text-2xl font-semibold">Тренды</h2>
<h2 style={interSemiboldStyle} className="text-white text-2xl">Тренды</h2>
<div className="relative">
<select
value={selectedTablePeriod}
@@ -248,63 +261,64 @@ const Dashboard: React.FC = () => {
<table className="w-full">
<thead>
<tr className="border-b border-gray-700">
<th className="text-left text-white font-medium py-3">Детектор</th>
<th className="text-left text-white font-medium py-3">Сообщение</th>
<th className="text-left text-white font-medium py-3">Серьезность</th>
<th className="text-left text-white font-medium py-3">Дата</th>
<th className="text-left text-white font-medium py-3">Решен</th>
<th style={interSemiboldStyle} className="text-left text-white text-sm py-3">Детектор</th>
<th style={interSemiboldStyle} className="text-left text-white text-sm py-3">Сообщение</th>
<th style={interSemiboldStyle} className="text-left text-white text-sm py-3">Серьезность</th>
<th style={interSemiboldStyle} className="text-left text-white text-sm py-3">Дата</th>
<th style={interSemiboldStyle} className="text-left text-white text-sm py-3">Решен</th>
</tr>
</thead>
<tbody>
{filteredAlerts.map((alert: any) => (
<tr key={alert.id} className="border-b border-gray-800">
<td className="py-3 text-white text-sm">{alert.name}</td>
<td className="py-3 text-gray-300 text-sm">{alert.message}</td>
<td style={interRegularStyle} className="py-3 text-white text-sm">{alert.name}</td>
<td style={interRegularStyle} className="py-3 text-gray-300 text-sm">{alert.message}</td>
<td className="py-3">
<span className={`text-sm ${alert.severity === 'critical' ? 'text-red-500' : alert.severity === 'warning' ? 'text-orange-500' : 'text-green-500'}`}>
<span style={interRegularStyle} className={`text-sm ${alert.severity === 'critical' ? 'text-red-500' : alert.severity === 'warning' ? 'text-orange-500' : 'text-green-500'}`}>
{alert.severity === 'critical' ? 'Критическое' : alert.severity === 'warning' ? 'Предупреждение' : 'Норма'}
</span>
</td>
<td className="py-3 text-gray-400 text-sm">{new Date(alert.created_at).toLocaleString()}</td>
<td style={interRegularStyle} className="py-3 text-gray-400 text-sm">{new Date(alert.created_at).toLocaleString()}</td>
<td className="py-3">
{alert.resolved ? (
<span className="text-sm text-green-500">Да</span>
<span style={interRegularStyle} className="text-sm text-green-500">Да</span>
) : (
<span className="text-sm text-gray-500">Нет</span>
)}
<span style={interRegularStyle} className="text-sm text-gray-500">Нет</span>
)
}
</td>
</tr>
))}
</tbody>
</table>
</div>
{/* Статы */}
<div className="mt-6 grid grid-cols-4 gap-4">
<div className="text-center">
<div className="text-2xl font-bold text-white">{filteredAlerts.length}</div>
<div className="text-sm text-gray-400">Всего</div>
</div>
<div className="text-center">
<div className="text-2xl font-bold text-green-500">{statusCounts.normal}</div>
<div className="text-sm text-gray-400">Норма</div>
</div>
<div className="text-center">
<div className="text-2xl font-bold text-orange-500">{statusCounts.warning}</div>
<div className="text-sm text-gray-400">Предупреждения</div>
</div>
<div className="text-center">
<div className="text-2xl font-bold text-red-500">{statusCounts.critical}</div>
<div className="text-sm text-gray-400">Критические</div>
</div>
</div>
</div>
</div>
</div>
</div>
{/* Статистика */}
<div className="mt-6 grid grid-cols-4 gap-4">
<div className="text-center">
<div style={interSemiboldStyle} className="text-2xl text-white">{filteredAlerts.length}</div>
<div style={interRegularStyle} className="text-sm text-gray-400">Всего</div>
</div>
<div className="text-center">
<div style={interSemiboldStyle} className="text-2xl text-green-500">{statusCounts.normal}</div>
<div style={interRegularStyle} className="text-sm text-gray-400">Норма</div>
</div>
<div className="text-center">
<div style={interSemiboldStyle} className="text-2xl text-orange-500">{statusCounts.warning}</div>
<div style={interRegularStyle} className="text-sm text-gray-400">Предупреждения</div>
</div>
<div className="text-center">
<div style={interSemiboldStyle} className="text-2xl text-red-500">{statusCounts.critical}</div>
<div style={interRegularStyle} className="text-sm text-gray-400">Критические</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
)
}
export default Dashboard
export default Dashboard