modified the skeleton MUI

pull/40/head
DmitriyA 2025-04-23 08:25:15 -04:00
parent e47161acd1
commit 40d8046617
3 changed files with 166 additions and 29 deletions

View File

@ -0,0 +1,34 @@
import React from 'react';
import { Box, Skeleton } from '@mui/material';
const ChartSkeleton = ({ count = 1 }) => {
return (
<>
{Array.from({ length: count }).map((_, index) => (
<Box
key={index}
sx={{
backgroundColor: '#fff',
borderRadius: '8px',
padding: '20px',
mb: 3,
height: '400px'
}}
>
<Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 2 }}>
<Skeleton variant="text" width="40%" height={30} />
<Skeleton variant="text" width="20%" height={30} />
</Box>
<Skeleton variant="rectangular" width="100%" height="80%" />
<Box sx={{ display: 'flex', justifyContent: 'center', mt: 2, gap: 2 }}>
{[1, 2, 3, 4].map((i) => (
<Skeleton key={i} variant="rounded" width={80} height={36} />
))}
</Box>
</Box>
))}
</>
);
};
export default ChartSkeleton;

View File

@ -1,6 +1,8 @@
import React, { useState, useRef, useEffect } from 'react';
import { LineChart, XAxis, YAxis, CartesianGrid, Tooltip, Line, ResponsiveContainer, ReferenceArea } from 'recharts';
import { Skeleton } from '@mui/material';
import { HOUR, DAY } from './constants';
const TIME_FORMATS = {
LONG: 'dd.MM HH:mm', // Для диапазона > 24 часов
MEDIUM: 'HH:mm', // Для диапазона > 1 часа
@ -51,7 +53,6 @@ const LineChartComponent = ({
? Object.keys(displayData[0]).filter(k => !['timestamp', 'time', 'fullTime'].includes(k))
: [];
// Функция для определения оптимального формата времени в зависимости от диапазона
const getTimeFormat = () => {
if (!data.length) return TIME_FORMATS.SHORT;
@ -77,7 +78,6 @@ const LineChartComponent = ({
const handleMouseDown = (e) => {
if (!e) return;
// Получаем индекс точки по координатам
const activeIndex = e.activeTooltipIndex;
if (activeIndex === undefined || activeIndex < 0 || activeIndex >= data.length) return;
@ -113,7 +113,6 @@ const LineChartComponent = ({
const startIndex = Math.min(selectionArea.startIndex, selectionArea.endIndex);
const endIndex = Math.max(selectionArea.startIndex, selectionArea.endIndex);
// Нормализуем индексы к диапазону [0, 1] для родительского компонента
const normalizedStart = startIndex / (data.length - 1);
const normalizedEnd = endIndex / (data.length - 1);
@ -152,7 +151,18 @@ const LineChartComponent = ({
};
if (!data.length) {
return <div style={{ padding: '20px', textAlign: 'center' }}>Нет данных для отображения</div>;
return (
<Box sx={{
position: 'relative',
height: '400px',
backgroundColor: '#fff',
borderRadius: '8px',
padding: '20px'
}}>
<Skeleton variant="text" width="60%" height={30} sx={{ mb: 2 }} />
<Skeleton variant="rectangular" width="100%" height="calc(100% - 50px)" />
</Box>
);
}
return (

View File

@ -1,13 +1,23 @@
import React, { useEffect, useRef, useState } from 'react';
import React, { useEffect, useRef, useState, useCallback } from 'react';
import Box from '@mui/material/Box';
import Skeleton from '@mui/material/Skeleton';
const LazyChartBatchRenderer = ({ charts }) => {
const [visibleIndices, setVisibleIndices] = useState(new Set());
const placeholderRefs = useRef([]);
const observerRef = useRef(null);
const cleanupTimeoutRef = useRef(null);
const ChartSkeleton = () => (
<Box sx={{ backgroundColor: '#fff', borderRadius: '8px', padding: '20px', marginBottom: '20px', position: 'relative', height: '500px' }}>
<Box sx={{
backgroundColor: '#fff',
borderRadius: '8px',
padding: '20px',
marginBottom: '20px',
position: 'relative',
height: '400px',
overflow: 'hidden'
}}>
<Box sx={{ position: 'absolute', right: '20px', top: '20px' }}>
<Skeleton variant="circular" width={16} height={16} />
</Box>
@ -15,46 +25,124 @@ const LazyChartBatchRenderer = ({ charts }) => {
<Skeleton variant="text" width="40%" height={30} />
<Skeleton variant="text" width="30%" height={30} />
</Box>
<Skeleton variant="rectangular" width="100%" height={300} />
<Skeleton
variant="rectangular"
width="100%"
height="300px"
sx={{
transform: 'none',
animation: 'none'
}}
/>
<Box sx={{ display: 'flex', justifyContent: 'center', mt: 2, gap: 2 }}>
{[1, 2, 3, 4].map((_, i) => (
<Skeleton key={i} variant="rounded" width={80} height={36} />
<Skeleton
key={i}
variant="rounded"
width={80}
height={36}
sx={{
transform: 'none',
animation: 'none'
}}
/>
))}
</Box>
</Box>
);
useEffect(() => {
const observer = new IntersectionObserver(
(entries) => {
setVisibleIndices((prev) => {
const updated = new Set(prev);
entries.forEach((entry) => {
const index = parseInt(entry.target.dataset.index, 10);
if (entry.isIntersecting) {
updated.add(index);
} else {
updated.delete(index);
const isElementFarFromViewport = useCallback((element) => {
if (!element) return true;
const rect = element.getBoundingClientRect();
const buffer = window.innerHeight * 1.5;
return rect.bottom < -buffer || rect.top > window.innerHeight + buffer;
}, []);
const updateVisibleIndices = useCallback(() => {
const newVisibleIndices = new Set();
placeholderRefs.current.forEach((ref, index) => {
if (ref && !isElementFarFromViewport(ref)) {
newVisibleIndices.add(index);
}
});
return updated;
setVisibleIndices(prev => {
if (newVisibleIndices.size === prev.size &&
Array.from(newVisibleIndices).every(i => prev.has(i))) {
return prev;
}
return newVisibleIndices;
});
}, [isElementFarFromViewport]);
useEffect(() => {
observerRef.current = new IntersectionObserver(
(entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
updateVisibleIndices();
}
});
},
{
root: null,
rootMargin: '200px',
threshold: 0.1,
rootMargin: '500px 0px',
threshold: 0.01
}
);
placeholderRefs.current.forEach((ref) => {
if (ref) observer.observe(ref);
placeholderRefs.current.forEach(ref => {
if (ref) observerRef.current.observe(ref);
});
return () => {
observer.disconnect();
const handleScroll = () => {
if (cleanupTimeoutRef.current) {
clearTimeout(cleanupTimeoutRef.current);
}
cleanupTimeoutRef.current = setTimeout(() => {
updateVisibleIndices();
setVisibleIndices(prev => {
const updated = new Set(prev);
let changed = false;
placeholderRefs.current.forEach((ref, index) => {
if (ref && isElementFarFromViewport(ref) && prev.has(index)) {
updated.delete(index);
changed = true;
}
});
return changed ? updated : prev;
});
}, 150);
};
window.addEventListener('scroll', handleScroll, { passive: true });
window.addEventListener('resize', updateVisibleIndices, { passive: true });
updateVisibleIndices();
return () => {
if (cleanupTimeoutRef.current) clearTimeout(cleanupTimeoutRef.current);
if (observerRef.current) observerRef.current.disconnect();
window.removeEventListener('scroll', handleScroll);
window.removeEventListener('resize', updateVisibleIndices);
};
}, [updateVisibleIndices]);
const shouldShowChart = (index) => {
return visibleIndices.has(index) ||
visibleIndices.has(index - 1) ||
visibleIndices.has(index + 1);
};
}, [charts]);
return (
<div>
@ -63,12 +151,17 @@ const LazyChartBatchRenderer = ({ charts }) => {
key={index}
ref={(el) => (placeholderRefs.current[index] = el)}
data-index={index}
style={{
minHeight: '400px',
marginBottom: '20px',
transition: 'opacity 0.3s ease',
}}
>
{visibleIndices.has(index) ? chart : <ChartSkeleton />}
{shouldShowChart(index) ? chart : <ChartSkeleton />}
</div>
))}
</div>
);
};
export default LazyChartBatchRenderer;
export default React.memo(LazyChartBatchRenderer);