From 5c3031f5f52e4a7bbe101441a97f5e47dd1eff3f Mon Sep 17 00:00:00 2001 From: c0mrade Date: Wed, 19 Aug 2026 17:58:54 +0300 Subject: [PATCH] =?UTF-8?q?fix(geocheck):=20=D1=81=D0=BA=D0=B5=D0=BB=D0=B5?= =?UTF-8?q?=D1=82=D0=BE=D0=BD=20=D0=B4=D0=BE=20=D0=BE=D1=82=D1=80=D0=B8?= =?UTF-8?q?=D1=81=D0=BE=D0=B2=D0=BA=D0=B8=20=D0=BE=D1=82=D1=87=D1=91=D1=82?= =?UTF-8?q?=D0=B0=20=D0=B8=20=D1=81=D0=B1=D1=80=D0=BE=D1=81=20=D0=B7=D1=83?= =?UTF-8?q?=D0=BC=D0=B0=20=D0=BD=D0=B0=20=D0=BD=D0=BE=D0=B2=D0=BE=D0=BC=20?= =?UTF-8?q?=D0=BE=D1=82=D1=87=D1=91=D1=82=D0=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Отчёт весит сотни килобайт вместе со встроенным шрифтом, поэтому между приходом данных и первой отрисовкой есть заметная пауза. Всё это время на экране была пустая тёмная коробка, зато с готовыми кнопками зума, которым нечего масштабировать. Теперь до отрисовки показывается скелетон, повторяющий ритм отчёта, а панель зума появляется вместе с картинкой. Перезапуск проверки наследовал зум и сдвиг от предыдущего отчёта. Новый отчёт должен открываться в исходном масштабе, поэтому его подпись входит в ключ просмотрщика. Подпись — хеш всей строки, посчитанный один раз на отчёт. Первая попытка брала длину и хвост base64, и этого оказалось мало: отчёты одного узла кончаются одинаково, подписи совпадали, зум не сбрасывался. Поймано проверкой, а не рассуждением. Готовность хранится как «какая картинка отрисована», а не булевым флагом: тогда новый отчёт автоматически считается незагруженным и сбрасывающий эффект не нужен. Проверено в браузере: при задержке картинки — скелетон есть, панели зума нет; после загрузки наоборот. Зум со сдвигом после перезапуска возвращается к исходному преобразованию, дважды подряд. Co-Authored-By: Claude Opus 5 (1M context) --- .../admin/remnawave/GeoCheckImageViewer.tsx | 107 +++++++++++++----- 1 file changed, 77 insertions(+), 30 deletions(-) diff --git a/src/components/admin/remnawave/GeoCheckImageViewer.tsx b/src/components/admin/remnawave/GeoCheckImageViewer.tsx index 253cffc..4775487 100644 --- a/src/components/admin/remnawave/GeoCheckImageViewer.tsx +++ b/src/components/admin/remnawave/GeoCheckImageViewer.tsx @@ -1,4 +1,4 @@ -import { useLayoutEffect, useRef, useState } from 'react'; +import { useLayoutEffect, useMemo, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { TransformComponent, TransformWrapper } from 'react-zoom-pan-pinch'; import { MinusIcon, PlusIcon, ResetIcon } from '@/components/icons'; @@ -12,6 +12,9 @@ import { MinusIcon, PlusIcon, ResetIcon } from '@/components/icons'; const READABLE_WIDTH = 760; /** Масштаб 1 — отчёт вписан в ширину области; ниже опускаться незачем. */ +/** Ширины строк-заглушек, повторяющие ритм отчёта: заголовок, строки, пробел. */ +const SKELETON_ROWS = [42, 88, 80, 84, 30, 70, 76, 82, 64, 28, 86, 74, 80, 68]; + const SCALE_MIN = 1; const SCALE_MAX = 8; @@ -37,6 +40,27 @@ export function GeoCheckImageViewer({ src, alt, fullscreen }: GeoCheckImageViewe const { t } = useTranslation(); const hostRef = useRef(null); const [width, setWidth] = useState(0); + // Отчёт весит сотни килобайт вместе со встроенным шрифтом, и между приходом + // данных и первой отрисовкой есть заметная пауза. До неё показываем + // скелетон, а не пустую тёмную коробку с кнопками зума. + // + // Готовность хранится как «какая картинка отрисована», а не флагом: тогда + // новый отчёт автоматически считается незагруженным, без сбрасывающего + // эффекта. + const [loadedSrc, setLoadedSrc] = useState(null); + const loaded = loadedSrc === src; + + // Подпись отчёта для ключа. Длины и хвоста base64 недостаточно: отчёты + // одного узла кончаются одинаково и совпали бы по такой подписи, а зум + // тогда не сбрасывается. Полная строка в ключе — сотни килобайт на каждый + // рендер, поэтому считаем хеш один раз на отчёт. + const srcId = useMemo(() => { + let hash = 0; + for (let i = 0; i < src.length; i += 1) { + hash = (hash * 31 + src.charCodeAt(i)) | 0; + } + return `${src.length}:${hash}`; + }, [src]); useLayoutEffect(() => { const host = hostRef.current; @@ -57,13 +81,27 @@ export function GeoCheckImageViewer({ src, alt, fullscreen }: GeoCheckImageViewe return (
+ {!loaded && ( +
+ {SKELETON_ROWS.map((w) => ( +
+ ))} +
+ )} {width > 0 && ( - {alt} + {alt} setLoadedSrc(src)} + onError={() => setLoadedSrc(src)} + /> -
- - - - -
+ {loaded && ( +
+ + + + +
+ )} )}