Compare commits
6 Commits
9ca1b2e20a
...
8bb19f3f9d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8bb19f3f9d | ||
| 4e3cc0eb33 | |||
|
|
01d05b4331 | ||
| ca78c0b17b | |||
|
|
19ca8d0885 | ||
| ba8bb6940b |
@@ -1,3 +1,4 @@
|
|||||||
|
import mpegts from 'mpegts.js';
|
||||||
import { useEffect, useRef, useState } from 'react';
|
import { useEffect, useRef, useState } from 'react';
|
||||||
|
|
||||||
type CameraViewProps = {
|
type CameraViewProps = {
|
||||||
@@ -11,8 +12,7 @@ export function CameraView({ title = 'Камера', wsUrl }: CameraViewProps) {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const video = videoRef.current;
|
const video = videoRef.current;
|
||||||
let player: import('mpegts.js').default.Player | null = null;
|
let player: ReturnType<typeof mpegts.createPlayer> | null = null;
|
||||||
let cancelled = false;
|
|
||||||
|
|
||||||
if (!video) {
|
if (!video) {
|
||||||
return;
|
return;
|
||||||
@@ -22,14 +22,11 @@ export function CameraView({ title = 'Камера', wsUrl }: CameraViewProps) {
|
|||||||
video.addEventListener('playing', clearError);
|
video.addEventListener('playing', clearError);
|
||||||
setError(null);
|
setError(null);
|
||||||
|
|
||||||
void import('mpegts.js').then(({ default: mpegts }) => {
|
|
||||||
if (cancelled) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!mpegts.isSupported()) {
|
if (!mpegts.isSupported()) {
|
||||||
setError('Браузер не поддерживает MPEG-TS поток');
|
setError('Браузер не поддерживает MPEG-TS поток');
|
||||||
return;
|
return () => {
|
||||||
|
video.removeEventListener('playing', clearError);
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
player = mpegts.createPlayer(
|
player = mpegts.createPlayer(
|
||||||
@@ -44,10 +41,8 @@ export function CameraView({ title = 'Камера', wsUrl }: CameraViewProps) {
|
|||||||
if (playResult instanceof Promise) {
|
if (playResult instanceof Promise) {
|
||||||
void playResult.then(() => setError(null)).catch(() => setError('Не удалось запустить воспроизведение'));
|
void playResult.then(() => setError(null)).catch(() => setError('Не удалось запустить воспроизведение'));
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
cancelled = true;
|
|
||||||
video.removeEventListener('playing', clearError);
|
video.removeEventListener('playing', clearError);
|
||||||
player?.pause();
|
player?.pause();
|
||||||
player?.unload();
|
player?.unload();
|
||||||
|
|||||||
@@ -4,8 +4,9 @@ import { useRef } from 'react';
|
|||||||
import { CalendarDays, CalendarClock, Check, ChevronDown, Clock3, DatabaseZap, Search } from 'lucide-react';
|
import { CalendarDays, CalendarClock, Check, ChevronDown, Clock3, DatabaseZap, Search } from 'lucide-react';
|
||||||
import type { CurrentItem } from '../../../entities/current/types';
|
import type { CurrentItem } from '../../../entities/current/types';
|
||||||
import { HistoryChart } from '../../../features/history-chart/HistoryChart';
|
import { HistoryChart } from '../../../features/history-chart/HistoryChart';
|
||||||
|
import type { HistoryZoomRange } from '../../../features/history-chart/chartTypes';
|
||||||
import { RANGE_PRESETS, createRange, type DateRangeState } from '../../../features/history/dateRange';
|
import { RANGE_PRESETS, createRange, type DateRangeState } from '../../../features/history/dateRange';
|
||||||
import { toIsoFromInput } from '../../../utils/format';
|
import { toInputDateTimeValue, toIsoFromInput } from '../../../utils/format';
|
||||||
import type { HistoryGranularity } from '../../../utils/historyGranularity';
|
import type { HistoryGranularity } from '../../../utils/historyGranularity';
|
||||||
|
|
||||||
type ArchiveViewProps = {
|
type ArchiveViewProps = {
|
||||||
@@ -108,6 +109,12 @@ export function ArchiveView({
|
|||||||
[rangePart]: updateRangeInputPart(range[rangePart], inputPart, value),
|
[rangePart]: updateRangeInputPart(range[rangePart], inputPart, value),
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
const updateRangeFromZoom = (nextRange: HistoryZoomRange) => {
|
||||||
|
onRangeChange({
|
||||||
|
from: toInputDateTimeValue(new Date(nextRange.from)),
|
||||||
|
to: toInputDateTimeValue(new Date(nextRange.to)),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="chart-section chart-section--archive">
|
<section className="chart-section chart-section--archive">
|
||||||
@@ -263,6 +270,7 @@ export function ArchiveView({
|
|||||||
labelFormat={historyAxis.labelFormat}
|
labelFormat={historyAxis.labelFormat}
|
||||||
tags={selectedTags}
|
tags={selectedTags}
|
||||||
tagLabels={tagLabels}
|
tagLabels={tagLabels}
|
||||||
|
onZoomRangeChange={updateRangeFromZoom}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div className="chart-placeholder">Разверните список показателей и выберите серию для графика</div>
|
<div className="chart-placeholder">Разверните список показателей и выберите серию для графика</div>
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ type HistoryChartProps = {
|
|||||||
tickIntervalMs?: number;
|
tickIntervalMs?: number;
|
||||||
to: string;
|
to: string;
|
||||||
tagLabels?: Record<string, string>;
|
tagLabels?: Record<string, string>;
|
||||||
|
onZoomRangeChange?: (range: HistoryZoomRange) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const ZOOM_REQUEST_DELAY_MS = 350;
|
const ZOOM_REQUEST_DELAY_MS = 350;
|
||||||
@@ -95,6 +96,7 @@ export function HistoryChart({
|
|||||||
tickIntervalMs,
|
tickIntervalMs,
|
||||||
to,
|
to,
|
||||||
tagLabels = {},
|
tagLabels = {},
|
||||||
|
onZoomRangeChange,
|
||||||
}: HistoryChartProps) {
|
}: HistoryChartProps) {
|
||||||
const [zoomRange, setZoomRange] = useState(() =>
|
const [zoomRange, setZoomRange] = useState(() =>
|
||||||
createInitialZoomRange({ from, granulate, labelFormat, tickIntervalMs, to }),
|
createInitialZoomRange({ from, granulate, labelFormat, tickIntervalMs, to }),
|
||||||
@@ -167,8 +169,9 @@ export function HistoryChart({
|
|||||||
zoomTimerRef.current = setTimeout(() => {
|
zoomTimerRef.current = setTimeout(() => {
|
||||||
zoomRangeRef.current = nextRange;
|
zoomRangeRef.current = nextRange;
|
||||||
setZoomRange(nextRange);
|
setZoomRange(nextRange);
|
||||||
|
onZoomRangeChange?.(nextRange);
|
||||||
}, ZOOM_REQUEST_DELAY_MS);
|
}, ZOOM_REQUEST_DELAY_MS);
|
||||||
}, []);
|
}, [onZoomRangeChange]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<HistoryChartArea
|
<HistoryChartArea
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ function formatChartValue(value: number): string {
|
|||||||
}).format(value);
|
}).format(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Показывает одно значение, если min/avg/max визуально совпадают. */
|
/** Форматирует значение tooltip: одно число для min=avg=max или среднее с диапазоном. */
|
||||||
function formatTooltipValue(value: AvgPointValue): string {
|
function formatTooltipValue(value: AvgPointValue): string {
|
||||||
const avg = formatChartValue(value[1]);
|
const avg = formatChartValue(value[1]);
|
||||||
const min = formatChartValue(value[2]);
|
const min = formatChartValue(value[2]);
|
||||||
@@ -70,7 +70,7 @@ function formatTooltipValue(value: AvgPointValue): string {
|
|||||||
return avg;
|
return avg;
|
||||||
}
|
}
|
||||||
|
|
||||||
return `min ${min} · avg ${avg} · max ${max}`;
|
return `сред. ${avg} (${min} .. ${max})`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function isAvgPointValue(value: unknown): value is AvgPointValue {
|
function isAvgPointValue(value: unknown): value is AvgPointValue {
|
||||||
|
|||||||
Reference in New Issue
Block a user