From 32af8975f992f6f108481aab975dae3558dc1790 Mon Sep 17 00:00:00 2001 From: Viktor Antropov Date: Wed, 9 Sep 2026 13:53:17 +0300 Subject: [PATCH] =?UTF-8?q?ui:=20=D0=BF=D0=B5=D1=80=D0=B5=D1=80=D0=B0?= =?UTF-8?q?=D0=B1=D0=BE=D1=82=D0=BA=D0=B0=20=D1=81=D1=82=D1=80=D0=B0=D0=BD?= =?UTF-8?q?=D0=B8=D1=86=D1=8B=20=D1=80=D0=B5=D0=B7=D1=83=D0=BB=D1=8C=D1=82?= =?UTF-8?q?=D0=B0=D1=82=D0=BE=D0=B2=20=E2=80=94=20=D0=BB=D0=BE=D0=B3=D0=B8?= =?UTF-8?q?=D1=87=D0=B5=D1=81=D0=BA=D0=B8=D0=B5=20=D0=B1=D0=BB=D0=BE=D0=BA?= =?UTF-8?q?=D0=B8,=20=D1=82=D0=B0=20=D0=B6=D0=B5=20=D0=BF=D0=B0=D0=BB?= =?UTF-8?q?=D0=B8=D1=82=D1=80=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Проблема: 10 инфо-карточек в один столбец, карта с зонами — в самом низу, немедленные действия терялись в середине, формула занимала пол-экрана раньше вывода. Новая структура (сверху вниз = порядок чтения штабом): 1. Шапка-панель решения: срочность | радиус (+множитель профилей отдельным чипом) | критические действия списком — решение видно без прокрутки; summary свёрнут в одну строку. 2. Главный ряд: КАРТА (2/3) + список ЗОН (1/3) рядом — зоны и карта теперь одно визуальное целое; geo-чипы (💧/🚂) и предупреждения геослоя компактными плашками в шапке карты. 3. Ряд поведение+действия: психотип (тактика 2×2 сеткой), теги профилей, unmodeled-пометки; немедленные действия нумерованным списком, КРИТИЧНО-пункты акцентной рамкой. 4. Ряд обоснование радиуса: формула моно-шрифтом + коэффициенты чипами (НормС/СП/СКД/СУ/СУТ/ВВС/ВП с подсказками) + психотип- поправки полос чипами. 5. Низ: справка (обстоятельства/здоровье/среда) сеткой 3 колонки. Цветовая схема не тронута: var(--accent) #ff4d00, зоны #ff4d00/ #ff9f1a/#ffd166, тёмная тема ЦОУ. Направления зон — кириллицей (С/СВ/В/...). Все поля бэкенда рендерятся (проверено живым analyze). --- .../src/pages/analysis/AnalysisResult.css | 688 +++++++++++++----- .../src/pages/analysis/AnalysisResult.jsx | 421 ++++++----- 2 files changed, 714 insertions(+), 395 deletions(-) diff --git a/frontend/src/pages/analysis/AnalysisResult.css b/frontend/src/pages/analysis/AnalysisResult.css index c384c34..081648b 100644 --- a/frontend/src/pages/analysis/AnalysisResult.css +++ b/frontend/src/pages/analysis/AnalysisResult.css @@ -4,19 +4,24 @@ color: var(--text-primary); } -/* Header */ +/* ===== Header: кейс + панель решения ===== */ .result-header { background: linear-gradient(135deg, rgba(255, 77, 0, 0.1) 0%, rgba(255, 77, 0, 0.05) 100%); border-bottom: 2px solid rgba(255, 77, 0, 0.3); - padding: 32px 24px; - margin-bottom: 32px; + padding: 24px 24px 20px; + margin-bottom: 24px; +} + +.header-top { + max-width: 1400px; + margin: 0 auto; } .result-header h1 { - font-size: 36px; + font-size: 28px; font-weight: 700; color: var(--text-primary); - margin: 0 0 16px 0; + margin: 0 0 10px 0; display: flex; align-items: center; gap: 12px; @@ -30,8 +35,8 @@ .case-info { display: flex; flex-wrap: wrap; - gap: 16px; - font-size: 16px; + gap: 12px; + font-size: 14px; color: var(--text-secondary); align-items: center; } @@ -41,167 +46,244 @@ font-weight: 700; } -/* Content */ +/* Панель решения: срочность | радиус | критические действия */ +.decision-strip { + max-width: 1400px; + margin: 18px 0 0 0; + display: grid; + grid-template-columns: 200px 220px 1fr; + gap: 12px; +} + +.decision-cell { + background: var(--bg-secondary); + border: 1px solid rgba(255, 77, 0, 0.25); + border-radius: 10px; + padding: 12px 16px; + min-width: 0; +} + +.decision-label { + font-size: 12px; + text-transform: uppercase; + letter-spacing: 0.6px; + color: var(--text-secondary); + margin-bottom: 6px; +} + +.decision-value { + font-size: 24px; + font-weight: 700; + color: var(--text-primary); + display: flex; + align-items: baseline; + gap: 8px; + line-height: 1.2; +} + +.radius-mult { + font-size: 14px; + font-weight: 600; + color: var(--accent); + background: rgba(255, 77, 0, 0.12); + padding: 2px 8px; + border-radius: 6px; +} + +/* Срочность — та же цветовая схема (акцентные тона ЦОУ) */ +.urgency-cell.urgency-критический { + border-color: #ff4d00; + background: rgba(255, 77, 0, 0.12); +} +.urgency-cell.urgency-критический .decision-value { color: #ff4d00; } + +.urgency-cell.urgency-высокий { + border-color: #ff9f1a; + background: rgba(255, 159, 26, 0.12); +} +.urgency-cell.urgency-высокий .decision-value { color: #ff9f1a; } + +.urgency-cell.urgency-средний { + border-color: #ffd166; + background: rgba(255, 209, 102, 0.1); +} +.urgency-cell.urgency-средний .decision-value { color: #ffd166; } + +.urgency-cell.urgency-стандартный .decision-value { color: var(--text-primary); } + +.warnings-cell { display: flex; flex-direction: column; } + +.decision-warnings { + margin: 0; + padding: 0; + list-style: none; + display: flex; + flex-direction: column; + gap: 4px; + font-size: 13px; + line-height: 1.45; + color: var(--text-primary); +} + +.decision-warnings li::before { + content: '🚨 '; + color: #ff4d00; +} + +.decision-warnings.muted { + color: var(--text-secondary); + font-size: 13px; +} + +.header-summary { + max-width: 1400px; + margin: 14px 0 0 0; + font-size: 14px; + line-height: 1.6; + color: var(--text-secondary); +} + +/* ===== Content ===== */ .result-content { max-width: 1400px; margin: 0 auto; padding: 0 24px 48px; display: flex; flex-direction: column; - gap: 24px; + gap: 20px; } -/* Urgency Block */ -.urgency-block { - padding: 24px; - border-radius: 12px; - border: 2px solid; +/* ===== Ряд 1: карта + зоны ===== */ +.main-row { + display: grid; + gap: 16px; + align-items: start; +} + +.main-row.with-map { + grid-template-columns: minmax(0, 1.7fr) minmax(320px, 1fr); +} + +.main-row.no-map { + grid-template-columns: 1fr; +} + +.section-head { display: flex; - flex-direction: column; + align-items: center; + justify-content: space-between; gap: 12px; + margin-bottom: 12px; } -.urgency-block.urgency-высокий { - background: rgba(255, 77, 0, 0.15); - border-color: #ff4d00; +.section-head h2, +.info-card h3 { + font-size: 17px; + font-weight: 600; + color: var(--text-primary); + margin: 0; } -.urgency-block.urgency-средний { - background: rgba(255, 159, 26, 0.15); - border-color: #ff9f1a; -} - -.urgency-block.urgency-стандартный { - background: rgba(255, 209, 102, 0.15); - border-color: #ffd166; -} - -.urgency-level { - font-size: 24px; - font-weight: 700; - letter-spacing: 0.5px; -} - -.urgency-block.urgency-высокий .urgency-level { - color: #ff4d00; -} - -.urgency-block.urgency-средний .urgency-level { - color: #ff9f1a; -} - -.urgency-block.urgency-стандартный .urgency-level { - color: #ffd166; -} - -.urgency-reason { - font-size: 16px; - line-height: 1.6; - color: var(--text-secondary); -} - -/* Info Cards */ +.map-section, +.zones-panel, .info-card { background: var(--bg-secondary); border: 1px solid rgba(255, 77, 0, 0.2); border-radius: 12px; - padding: 24px; + padding: 18px; + min-width: 0; } -.info-card h3 { - font-size: 20px; - font-weight: 600; - color: var(--text-primary); - margin: 0 0 20px 0; +/* Гео-чипы в шапке карты */ +.geo-chips { display: flex; - align-items: center; gap: 8px; } -.info-card p { - margin: 12px 0; - line-height: 1.8; +.geo-chip { + font-size: 12px; + font-weight: 600; + padding: 4px 10px; + border-radius: 6px; + border: 1px solid var(--border); + color: var(--text-secondary); + background: var(--input-bg); +} + +.geo-chip.water { border-color: rgba(47, 129, 247, 0.5); color: #79b8ff; } +.geo-chip.rail { border-color: rgba(138, 47, 47, 0.7); color: #ff9d9d; } + +/* Предупреждения геослоя — компактные плашки */ +.geo-warning { + font-size: 13px; + line-height: 1.5; + color: #ff4d00; + background: rgba(255, 77, 0, 0.08); + border: 1px solid rgba(255, 77, 0, 0.4); + border-radius: 8px; + padding: 8px 12px; + margin-bottom: 10px; +} + +.geo-warning.rail { + color: #ff7b72; + border-color: rgba(138, 47, 47, 0.7); + background: rgba(138, 47, 47, 0.12); +} + +.map-empty { + height: 320px; + display: flex; + align-items: center; + justify-content: center; + color: var(--text-secondary); + background: var(--input-bg); + border-radius: 8px; + border: 1px dashed var(--border); + font-size: 14px; +} + +.map-info { + margin-top: 10px; + display: flex; + flex-wrap: wrap; + gap: 8px; + font-size: 13px; color: var(--text-secondary); } -.info-card p strong { - color: var(--text-primary); - font-weight: 600; -} +.map-info .sep { color: rgba(255, 77, 0, 0.5); } -.info-card hr { - margin: 16px 0; - border: none; - border-top: 1px solid rgba(255, 77, 0, 0.2); -} - -/* Distance Info */ -.distance-info { - display: grid; - gap: 24px; -} - -.distance-value { - font-size: 48px; - font-weight: 700; - color: var(--accent); - text-align: center; - padding: 24px; - background: rgba(255, 77, 0, 0.1); - border-radius: 12px; - border: 2px solid rgba(255, 77, 0, 0.3); -} - -.distance-details { - display: grid; - gap: 8px; -} - -.distance-details p { - padding: 12px 16px; - background: rgba(255, 77, 0, 0.05); - border-radius: 8px; - border-left: 3px solid var(--accent); - margin: 0; - font-size: 15px; -} - -/* Zones Section */ -.zones-section { - background: var(--bg-secondary); - border: 1px solid rgba(255, 77, 0, 0.2); - border-radius: 12px; - padding: 24px; -} - -.zones-section h2 { - font-size: 24px; - font-weight: 600; - color: var(--text-primary); - margin: 0 0 24px 0; +/* ===== Зоны ===== */ +.zones-count { + font-size: 13px; + color: var(--text-secondary); } .zones-list { - display: grid; - gap: 16px; + display: flex; + flex-direction: column; + gap: 10px; + max-height: 560px; + overflow-y: auto; + padding-right: 4px; } .zone-card { - background: rgba(255, 77, 0, 0.05); - border-radius: 10px; - padding: 20px; - border-left: 4px solid; - transition: all 0.2s ease; + background: rgba(255, 77, 0, 0.04); + border-radius: 8px; + padding: 12px 14px; + border-left: 3px solid var(--border); + transition: background 0.15s ease; } .zone-card:hover { background: rgba(255, 77, 0, 0.08); - transform: translateX(4px); } .zone-card.priority-1 { border-left-color: #ff4d00; - background: rgba(255, 77, 0, 0.1); + background: rgba(255, 77, 0, 0.09); } .zone-card.priority-2 { @@ -216,95 +298,308 @@ display: flex; justify-content: space-between; align-items: center; - margin-bottom: 12px; - gap: 16px; + gap: 10px; } .zone-title { display: flex; align-items: center; - gap: 12px; - flex: 1; + gap: 10px; + min-width: 0; } .zone-priority { display: inline-flex; align-items: center; justify-content: center; - width: 32px; - height: 32px; + width: 26px; + height: 26px; background: var(--accent); color: var(--bg-primary); border-radius: 50%; font-weight: 700; - font-size: 16px; + font-size: 13px; flex-shrink: 0; } .zone-name { - font-size: 18px; + font-size: 15px; font-weight: 600; color: var(--text-primary); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; } -.zone-score { - font-size: 28px; - font-weight: 700; - color: var(--accent); +/* Направление / дистанция / score — справа компактно */ +.zone-meta { + display: flex; + align-items: center; + gap: 8px; flex-shrink: 0; } -.zone-details { - display: flex; - flex-wrap: wrap; - gap: 16px; - margin-bottom: 12px; - font-size: 14px; +.zone-dir { + font-size: 12px; + font-weight: 700; color: var(--text-secondary); + background: var(--input-bg); + border: 1px solid var(--border); + border-radius: 4px; + padding: 2px 6px; + min-width: 26px; + text-align: center; } -.zone-details .separator { - color: rgba(255, 77, 0, 0.5); +.zone-dist { + font-size: 13px; + color: var(--text-secondary); + font-variant-numeric: tabular-nums; +} + +.zone-score { + font-size: 17px; + font-weight: 700; + color: var(--accent); + min-width: 48px; + text-align: right; + font-variant-numeric: tabular-nums; } .zone-reasoning { - padding: 12px 16px; - background: rgba(255, 77, 0, 0.08); - border-radius: 6px; + margin-top: 8px; + font-size: 12.5px; + color: var(--text-secondary); + line-height: 1.5; +} + +.zones-empty { + padding: 24px; + text-align: center; + color: var(--text-secondary); font-size: 14px; +} + +/* ===== Ряд 2: поведение + действия ===== */ +.behavior-row { + display: grid; + grid-template-columns: minmax(0, 1.4fr) minmax(0, 1fr); + gap: 20px; + align-items: stretch; +} + +.profile-tags { + display: flex; + flex-wrap: wrap; + gap: 6px; +} + +.profile-tag { + font-size: 12px; + font-weight: 600; + color: var(--accent); + background: rgba(255, 77, 0, 0.1); + border: 1px solid rgba(255, 77, 0, 0.35); + border-radius: 12px; + padding: 3px 10px; +} + +.profile-tag.unmodeled { + color: var(--text-secondary); + border-color: var(--border); + background: var(--input-bg); +} + +.prediction { + margin: 12px 0 0 0; + font-size: 14.5px; + line-height: 1.6; + color: var(--text-primary); +} + +/* Тактика психотипа — сетка 2×2 */ +.tactics-grid { + margin-top: 12px; + display: grid; + grid-template-columns: 1fr 1fr; + gap: 10px; +} + +.tactic { + background: var(--input-bg); + border: 1px solid var(--border); + border-radius: 8px; + padding: 10px 12px; + font-size: 13px; + line-height: 1.5; + color: var(--text-secondary); +} + +.tactic-label { + font-size: 11px; + text-transform: uppercase; + letter-spacing: 0.5px; + color: rgba(255, 77, 0, 0.8); + margin-bottom: 4px; + font-weight: 600; +} + +.unmodeled-note { + margin-top: 10px; + padding: 8px 12px; + background: var(--input-bg); + border-left: 3px solid var(--border); + border-radius: 6px; + font-size: 12.5px; color: var(--text-secondary); line-height: 1.6; } -/* Map Section */ -.map-section { - background: var(--bg-secondary); - border: 1px solid rgba(255, 77, 0, 0.2); - border-radius: 12px; - padding: 24px; +.unmodeled-note p { margin: 2px 0; } + +/* Немедленные действия — нумерованный список с акцентом */ +.actions-list { + margin: 12px 0 0 0; + padding: 0; + list-style: none; + counter-reset: action; + display: flex; + flex-direction: column; + gap: 8px; } -.map-section h2 { - font-size: 24px; +.actions-list li { + counter-increment: action; + display: flex; + gap: 10px; + font-size: 14px; + line-height: 1.5; + color: var(--text-primary); + background: var(--input-bg); + border: 1px solid var(--border); + border-radius: 8px; + padding: 10px 12px; +} + +.actions-list li::before { + content: counter(action); + flex-shrink: 0; + display: inline-flex; + align-items: center; + justify-content: center; + width: 22px; + height: 22px; + border-radius: 50%; + background: var(--accent); + color: var(--bg-primary); + font-weight: 700; + font-size: 12px; +} + +.actions-list li.critical { + border-color: #ff4d00; + background: rgba(255, 77, 0, 0.1); + font-weight: 600; +} + +.actions-list li.critical::before { + background: #ff4d00; +} + +/* ===== Ряд 3: обоснование радиуса ===== */ +.radius-formula { + font-size: 12px; + color: var(--text-secondary); + font-family: ui-monospace, 'SF Mono', Menlo, monospace; +} + +.coeff-grid { + margin-top: 14px; + display: grid; + grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); + gap: 10px; +} + +.coeff-chip { + background: var(--input-bg); + border: 1px solid var(--border); + border-left: 3px solid var(--accent); + border-radius: 8px; + padding: 10px 12px; +} + +.coeff-name { + font-size: 13px; + color: var(--text-secondary); + display: flex; + justify-content: space-between; + align-items: baseline; + gap: 6px; +} + +.coeff-value { + font-size: 16px; + font-weight: 700; + color: var(--text-primary); + font-variant-numeric: tabular-nums; +} + +.coeff-hint { + margin-top: 3px; + font-size: 11.5px; + color: var(--text-secondary); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +/* Психотип-поправки */ +.psy-mods { + margin-top: 14px; + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 8px; +} + +.psy-mods-title { + font-size: 13px; + color: var(--text-secondary); + margin-right: 4px; +} + +.psy-mod-chip { + font-size: 12.5px; font-weight: 600; color: var(--text-primary); - margin: 0 0 20px 0; + background: rgba(255, 77, 0, 0.07); + border: 1px solid rgba(255, 77, 0, 0.25); + border-radius: 6px; + padding: 4px 10px; + font-variant-numeric: tabular-nums; } -.map-info { - margin-top: 16px; - padding: 16px; - background: rgba(255, 77, 0, 0.05); - border-radius: 8px; - font-size: 14px; +/* ===== Ряд 4: справочные данные ===== */ +.reference-grid { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 20px; + align-items: start; +} + +.info-card p { + margin: 8px 0; + font-size: 13.5px; + line-height: 1.6; color: var(--text-secondary); } -.map-info p { - margin: 4px 0; +.info-card p strong { + color: var(--text-primary); + font-weight: 600; } -/* Loading & Error States */ +/* ===== Loading & Error ===== */ .loading-state, .error-state { display: flex; @@ -354,45 +649,34 @@ } .error-state button:hover { - background: #ff6a1a; + background: var(--accent-hover); transform: translateY(-2px); } -/* Responsive */ -@media (max-width: 768px) { - .result-header { - padding: 24px 16px; +/* ===== Responsive ===== */ +@media (max-width: 1100px) { + .main-row.with-map { + grid-template-columns: 1fr; } - - .result-header h1 { - font-size: 28px; + .behavior-row { + grid-template-columns: 1fr; } - - .result-content { - padding: 0 16px 32px; - } - - .distance-value { - font-size: 36px; - padding: 20px; - } - - .zone-header { - flex-direction: column; - align-items: flex-start; - } - - .zone-score { - font-size: 24px; - } - - .case-info { - flex-direction: column; - align-items: flex-start; - gap: 8px; - } - - .separator { - display: none; + .reference-grid { + grid-template-columns: 1fr 1fr; } } + +@media (max-width: 768px) { + .result-header { padding: 20px 16px 16px; } + .result-header h1 { font-size: 22px; } + .decision-strip { + grid-template-columns: 1fr 1fr; + } + .warnings-cell { grid-column: 1 / -1; } + .result-content { padding: 0 16px 32px; } + .reference-grid { grid-template-columns: 1fr; } + .coeff-grid { grid-template-columns: 1fr 1fr; } + .case-info { flex-direction: column; align-items: flex-start; gap: 6px; } + .separator { display: none; } + .tactics-grid { grid-template-columns: 1fr; } +} \ No newline at end of file diff --git a/frontend/src/pages/analysis/AnalysisResult.jsx b/frontend/src/pages/analysis/AnalysisResult.jsx index d8f4402..7580142 100644 --- a/frontend/src/pages/analysis/AnalysisResult.jsx +++ b/frontend/src/pages/analysis/AnalysisResult.jsx @@ -5,10 +5,12 @@ import { apiFetch } from "../../api/client"; import "./AnalysisResult.css"; const GENDER_LABEL = { m: 'М', f: 'Ж' }; +const DIRECTION_RU = { N: 'С', NE: 'СВ', E: 'В', SE: 'ЮВ', S: 'Ю', SW: 'ЮЗ', W: 'З', NW: 'СЗ' }; // Маппинг уровня срочности бэкенда → CSS-класс (фронтовые классы в Cyrillic) const urgencyClass = (urgency) => { const u = (urgency || '').toLowerCase(); + if (u.includes('крит')) return 'критический'; if (u.includes('высок')) return 'высокий'; if (u.includes('сред')) return 'средний'; return 'стандартный'; @@ -119,228 +121,246 @@ const AnalysisResult = () => { const urgencyLevel = urgencyClass(analysisData?.urgency); const profiles = analysisData?.active_profiles || []; const warnings = analysisData?.critical_warnings || []; + const unmodeled = analysisData?.unmodeled_profiles || []; const tnpLat = caseData.tnp_lat || caseData.gps_lat; const tnpLon = caseData.tnp_lon || caseData.gps_lon; + const waterCount = (waterData?.features || []).filter(f => f.layer !== 'railway').length; + const railCount = (waterData?.features || []).filter(f => f.layer === 'railway').length; + + // Коэффициенты формулы Экстремум — компактные чипы (название: значение) + const coeffChips = [ + { label: 'Время', value: `${caseData.elapsed_hours || 1} ч`, hint: 'от потери' }, + { label: 'НормС', value: coeffs.base_speed, hint: `баз. скорость, ${caseData.age_years} лет` }, + { label: 'СП', value: coeffs.terrain, hint: caseData.terrain?.join(', ') || 'местность' }, + { label: 'СКД', value: coeffs.diagnosis, hint: caseData.diagnosis_type?.join(', ') || 'без диагноза' }, + { label: 'СУ', value: coeffs.urgency, hint: caseData.has_transport && caseData.has_transport !== 'none' ? caseData.has_transport : 'без транспорта' }, + { label: 'СУТ', value: coeffs.fatigue?.toFixed(2), hint: 'усталость' }, + { label: 'ВВС', value: coeffs.time_of_day, hint: analysisData?.time_of_day || 'время суток' }, + { label: 'ВП', value: coeffs.weather, hint: caseData.weather || caseData.precipitation || 'без осадков' }, + ]; + + // Средний ряд: карта + зоны рядом + const hasMap = tnpLat && tnpLon; return (
+ {/* ===== Шапка: кейс + срочность + радиус ===== */}
-

ВЕКТОР Результаты анализа

-
- {caseData.child_name || 'Ребёнок'}, {caseData.age_years} лет{genderLabel ? ` (${genderLabel})` : ''} - • - Время потери: {caseData.loss_time || 'не указано'} - • - Прошло: {caseData.elapsed_hours || 1} ч - {analysisData && ( - <> - • - - {analysisData.fallback_used ? 'Локальный расчёт' : 'ИИ-анализ'} - - - )} +
+

ВЕКТОР Результаты анализа

+
+ {caseData.child_name || 'Ребёнок'}, {caseData.age_years} лет{genderLabel ? ` (${genderLabel})` : ''} + • + Время потери: {caseData.loss_time || 'не указано'} + • + Прошло: {caseData.elapsed_hours || 1} ч + {analysisData && ( + <> + • + + {analysisData.fallback_used ? 'Локальный расчёт' : 'ИИ-анализ'} + + + )} +
+ + {/* Панель решения: срочность | радиус | действия */} +
+
+
Срочность
+
{analysisData?.urgency || '—'}
+
+
+
Радиус поиска
+
+ {analysisData?.max_distance_km} км + {analysisData?.distance_multiplier !== 1 && ( + ×{analysisData?.distance_multiplier} + )} +
+
+
+
Критические действия
+ {warnings.length > 0 ? ( +
    + {warnings.map((w, i) =>
  • {w.warning}
  • )} +
+ ) : ( +
Нет критических предупреждений
+ )} +
+
+ {analysisData?.summary &&

{analysisData.summary}

}
- {/* Блок 1: Уровень срочности */} - {analysisData?.urgency && ( -
-
{analysisData.urgency}
-
{analysisData.summary || ''}
-
- )} - {/* Блок 2: Расчёт по алгоритму Экстремум */} -
-

📏 Расчёт по алгоритму Экстремум

-
-
{analysisData?.max_distance_km} км
-
-

Формула: Distance = Time × НормС × СП × СКД × СУ × СУТ × ВВС × ВП

-
-

Время (Time): {caseData.elapsed_hours || 1} ч

-

Базовая скорость (НормС): {coeffs.base_speed} км/ч (возраст {caseData.age_years} лет)

-

Местность (СП): {coeffs.terrain} ({caseData.terrain?.join(', ') || 'не указана'})

-

Диагноз (СКД): {coeffs.diagnosis} {caseData.has_diagnosis ? `(${caseData.diagnosis_type?.join(', ')})` : '(нет)'}

-

Транспорт (СУ): {coeffs.urgency} {caseData.has_transport && caseData.has_transport !== 'none' ? `(${caseData.has_transport})` : '(нет)'}

-

Усталость (СУТ): {coeffs.fatigue?.toFixed(2)} (снижение 5%/час)

-

Время суток (ВВС): {coeffs.time_of_day} ({analysisData?.time_of_day})

-

Погода (ВП): {coeffs.weather} ({caseData.weather || caseData.precipitation || 'нет'})

-
-

Радиус поиска: {analysisData?.search_radius_km} км × {analysisData?.distance_multiplier} (профили)

-
-
-
- - {/* Блок 3: Активные профили и критические предупреждения */} - {(profiles.length > 0 || warnings.length > 0) && ( -
-

⚠️ Активные профили

- {profiles.map((p, i) => ( -
-

{p.name}

- {p.note &&

{p.note}

} - {p.critical_warning && ( -

🚨 {p.critical_warning}

- )} -
- ))} -
- )} - - {/* Блок 4: Психотип и модель поведения */} - {analysisData?.psychotype && psyMods && ( -
-

🧠 Психотип и модель поведения

-

Психотип: {analysisData.psychotype}

-

Модель движения: {psyMods.description}

-
-

Модификаторы зон:

-

- • 0-500м: ×{psyMods.zone_0_500}
- • 500-1500м: ×{psyMods.zone_500_1500}
- • 1500-2500м: ×{psyMods.zone_1500_2500}
- • 2500м+: ×{psyMods.zone_2500plus} -

-
- {psyRec && ( -
-

Тактика поиска:

-

Приоритетные зоны: {psyRec.priority_zones}

-

Патрун: {psyRec.search_pattern}

-

Ключевые объекты: {psyRec.key_locations}

-

Связь: {psyRec.communication}

-
- )} -
- )} - - {/* Блок 5: Обстоятельства потери */} -
-

📋 Обстоятельства потери

-

Причина: {caseData.reason || caseData.loss_reason || 'не указана'}

-

Последнее известное место: {caseData.last_known_place || 'не указано'}

- {caseData.direction &&

Направление движения: {caseData.direction}

} - {caseData.behavior_description && ( -

Описание: {caseData.behavior_description}

- )} - {analysisData?.behavioral_prediction && ( -

Поведенческий прогноз: {analysisData.behavioral_prediction}

- )} -
- - {/* Блок 6: Зоны поиска */} -
-

Приоритетные зоны поиска

-
- {zones.slice(0, 8).map((zone, idx) => ( -
-
-
- #{idx + 1} - {zone.name} -
-
{zone.score}%
-
-
- Направление: {zone.direction} - • - Дистанция: {zone.distance} -
-
{zone.reasoning}
-
- ))} -
-
- - {/* Блок 7: Немедленные действия */} - {analysisData?.immediate_actions?.length > 0 && ( -
-

🚑 Немедленные действия

-
    - {analysisData.immediate_actions.map((a, i) => ( -
  • {a}
  • - ))} -
-
- )} - - {/* Блок 8: Карта */} - {tnpLat && tnpLon && ( + {/* ===== Ряд 1: Карта + зоны ===== */} +
-

Карта зон поиска

+
+

Карта зон поиска

+ {(waterData?.count > 0) && ( +
+ 💧 Водоёмы: {waterCount} + {railCount > 0 && 🚂 Ж/д: {railCount}} +
+ )} +
{waterData?.warning && ( -
-

- 🚨 {waterData.warning} -

+
+ 🚨 {waterData.warning}
)} - {waterData?.railway_warning && (waterData.features || []).some((f) => f.layer === 'railway') && ( -
-

- 🚂 {waterData.railway_warning} -

+ {waterData?.railway_warning && railCount > 0 && ( +
+ 🚂 {waterData.railway_warning}
)} - {waterData?.count > 0 && ( -

- 💧 Водоёмы в зоне поиска: {waterData.features.filter(f => f.layer !== 'railway').length} - {' '}· 🚂 Ж/д линий: {waterData.features.filter(f => f.layer === 'railway').length} - {waterData.warning && ' — приоритетная проверка'} (данные OSM) -

+ {hasMap ? ( + + ) : ( +
ТНП не задана — карта недоступна
)} - -
-

Координаты ТНП: {tnpLat}, {tnpLon}

- {caseData.tnp_address &&

Адрес: {caseData.tnp_address}

} +
+ ТНП: {tnpLat ? Number(tnpLat).toFixed(5) : '—'}, {tnpLon ? Number(tnpLon).toFixed(5) : '—'} + {caseData.tnp_address && ·} + {caseData.tnp_address && {caseData.tnp_address}}
- )} - {/* Блок 9: Здоровье и особенности */} - {(caseData.has_diagnosis || caseData.cant_swim || caseData.fitness_level || caseData.has_transport) && ( -
-

🏥 Здоровье и особенности

- {caseData.has_diagnosis && ( -

Диагнозы: {caseData.diagnosis_type?.join(', ') || 'есть'}

+
+
+

Зоны поиска

+ {zones.length} зон +
+
+ {zones.slice(0, 8).map((zone, idx) => ( +
+
+
+ {idx + 1} + {zone.name} +
+
+ {DIRECTION_RU[zone.direction] || zone.direction} + {zone.distance} +
{zone.score}%
+
+
+ {zone.reasoning &&
{zone.reasoning}
} +
+ ))} + {zones.length === 0 &&
Зоны не рассчитаны
} +
+
+
+ + {/* ===== Ряд 2: Поведение + действия ===== */} +
+
+
+

Поведение и профиль

+ {(profiles.length > 0 || unmodeled.length > 0) && ( +
+ {profiles.map((p, i) => {p.name})} + {unmodeled.map((u, i) => {u.profile}*)} +
+ )} +
+ {analysisData?.behavioral_prediction && ( +

{analysisData.behavioral_prediction}

)} - {caseData.fitness_level && ( -

Физическая подготовка: {caseData.fitness_level}

+ {psyRec && ( +
+
Приоритетные зоны
{psyRec.priority_zones}
+
Паттерн поиска
{psyRec.search_pattern}
+
Ключевые объекты
{psyRec.key_locations}
+
Связь
{psyRec.communication}
+
)} - {caseData.cant_swim &&

⚠️ Не умеет плавать — приоритет ближайшим водоёмам

} - {caseData.has_transport && caseData.has_transport !== 'none' && ( -

Транспорт: {caseData.has_transport}

+ {unmodeled.length > 0 && ( +
+ {unmodeled.map((u, i) => ( +

• {u.profile}: {u.note}

+ ))} +
)} - {(analysisData?.unmodeled_profiles || []).length > 0 && ( -
-

- Вне поведенческой модели (на скоринг зон не влияют): -

- {analysisData.unmodeled_profiles.map((u, i) => ( -

- • {u.profile}: {u.note} -

+
+ +
+

Немедленные действия

+
    + {(analysisData?.immediate_actions || []).map((a, i) => ( +
  1. {a}
  2. + ))} +
+
+
+ + {/* ===== Ряд 3: Обоснование радиуса (формула Экстремум) ===== */} + {analysisData && ( +
+
+

Обоснование радиуса

+ Distance = Time × НормС × СП × СКД × СУ × СУТ × ВВС × ВП +
+
+ {coeffChips.map((c, i) => ( +
+
{c.label} {c.value}
+
{c.hint}
+
+ ))} +
+ {psyMods && ( +
+ Психотип {psychoRu(analysisData?.psychotype)} — поправки полос: + {['0–500 м', '500–1500 м', '1500–2500 м', '2500 м+'].map((band, i) => ( + {band}: ×{[psyMods.zone_0_500, psyMods.zone_500_1500, psyMods.zone_1500_2500, psyMods.zone_2500plus][i]} ))}
)}
)} - {/* Блок 10: Условия среды */} -
-

🌤 Условия среды

-

Сезон: {caseData.season || 'не указан'}

- {caseData.temperature_c != null &&

Температура: {caseData.temperature_c}°C

} -

Осадки: {caseData.weather || caseData.precipitation || 'нет'}

- {caseData.visibility &&

Видимость: {caseData.visibility}

} + {/* ===== Ряд 4: Справочные данные (3 колонки) ===== */} +
+
+

Обстоятельства потери

+

Причина: {caseData.reason || caseData.loss_reason || 'не указана'}

+

Последнее известное место: {caseData.last_known_place || 'не указано'}

+ {caseData.direction &&

Направление движения: {caseData.direction}

} + {caseData.behavior_description &&

Описание: {caseData.behavior_description}

} +
+ +
+

Здоровье и особенности

+ {caseData.has_diagnosis ? ( +

Диагнозы: {caseData.diagnosis_type?.join(', ')}

+ ) : ( +

Диагнозы: нет

+ )} + {caseData.fitness_level &&

Физподготовка: {caseData.fitness_level}

} + {caseData.cant_swim &&

⚠️ Не умеет плавать — приоритет ближайшим водоёмам

} + {caseData.has_transport && caseData.has_transport !== 'none' && ( +

Транспорт: {caseData.has_transport}

+ )} +
+ +
+

Условия среды

+

Сезон: {caseData.season || 'не указан'}

+ {caseData.temperature_c != null &&

Температура: {caseData.temperature_c}°C

} +

Осадки: {caseData.weather || caseData.precipitation || 'нет'}

+ {caseData.visibility &&

Видимость: {caseData.visibility}

} +
@@ -348,4 +368,19 @@ const AnalysisResult = () => { ); }; -export default AnalysisResult; +// РУ-названия психотипов (psychotype_service отдаёт slug'и) +const PSY_RU = { + dominant: 'Доминантный', + harmonic: 'Гармоничный', + anxious: 'Тревожный', + introvert_passive: 'Интроверт пассивный', + introvert_active: 'Интроверт активный', +}; +const psychoRu = (p) => PSY_RU[p] || p || ''; + +// Класс вёрстки главного ряда: карта+зоны / только зоны +function hasMapClass(hasMap) { + return hasMap ? 'with-map' : 'no-map'; +} + +export default AnalysisResult; \ No newline at end of file