ui: переработка страницы результатов — логические блоки, та же палитра
Проблема: 10 инфо-карточек в один столбец, карта с зонами — в самом низу, немедленные действия терялись в середине, формула занимала пол-экрана раньше вывода. Новая структура (сверху вниз = порядок чтения штабом): 1. Шапка-панель решения: срочность | радиус (+множитель профилей отдельным чипом) | критические действия списком — решение видно без прокрутки; summary свёрнут в одну строку. 2. Главный ряд: КАРТА (2/3) + список ЗОН (1/3) рядом — зоны и карта теперь одно визуальное целое; geo-чипы (💧/🚂) и предупреждения геослоя компактными плашками в шапке карты. 3. Ряд поведение+действия: психотип (тактика 2×2 сеткой), теги профилей, unmodeled-пометки; немедленные действия нумерованным списком, КРИТИЧНО-пункты акцентной рамкой. 4. Ряд обоснование радиуса: формула моно-шрифтом + коэффициенты чипами (НормС/СП/СКД/СУ/СУТ/ВВС/ВП с подсказками) + психотип- поправки полос чипами. 5. Низ: справка (обстоятельства/здоровье/среда) сеткой 3 колонки. Цветовая схема не тронута: var(--accent) #ff4d00, зоны #ff4d00/ #ff9f1a/#ffd166, тёмная тема ЦОУ. Направления зон — кириллицей (С/СВ/В/...). Все поля бэкенда рендерятся (проверено живым analyze).
This commit is contained in:
@@ -4,19 +4,24 @@
|
|||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Header */
|
/* ===== Header: кейс + панель решения ===== */
|
||||||
.result-header {
|
.result-header {
|
||||||
background: linear-gradient(135deg, rgba(255, 77, 0, 0.1) 0%, rgba(255, 77, 0, 0.05) 100%);
|
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);
|
border-bottom: 2px solid rgba(255, 77, 0, 0.3);
|
||||||
padding: 32px 24px;
|
padding: 24px 24px 20px;
|
||||||
margin-bottom: 32px;
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-top {
|
||||||
|
max-width: 1400px;
|
||||||
|
margin: 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.result-header h1 {
|
.result-header h1 {
|
||||||
font-size: 36px;
|
font-size: 28px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
margin: 0 0 16px 0;
|
margin: 0 0 10px 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
@@ -30,8 +35,8 @@
|
|||||||
.case-info {
|
.case-info {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: 16px;
|
gap: 12px;
|
||||||
font-size: 16px;
|
font-size: 14px;
|
||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
@@ -41,167 +46,244 @@
|
|||||||
font-weight: 700;
|
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 {
|
.result-content {
|
||||||
max-width: 1400px;
|
max-width: 1400px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
padding: 0 24px 48px;
|
padding: 0 24px 48px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 24px;
|
gap: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Urgency Block */
|
/* ===== Ряд 1: карта + зоны ===== */
|
||||||
.urgency-block {
|
.main-row {
|
||||||
padding: 24px;
|
display: grid;
|
||||||
border-radius: 12px;
|
gap: 16px;
|
||||||
border: 2px solid;
|
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;
|
display: flex;
|
||||||
flex-direction: column;
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
|
margin-bottom: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.urgency-block.urgency-высокий {
|
.section-head h2,
|
||||||
background: rgba(255, 77, 0, 0.15);
|
.info-card h3 {
|
||||||
border-color: #ff4d00;
|
font-size: 17px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-primary);
|
||||||
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.urgency-block.urgency-средний {
|
.map-section,
|
||||||
background: rgba(255, 159, 26, 0.15);
|
.zones-panel,
|
||||||
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 */
|
|
||||||
.info-card {
|
.info-card {
|
||||||
background: var(--bg-secondary);
|
background: var(--bg-secondary);
|
||||||
border: 1px solid rgba(255, 77, 0, 0.2);
|
border: 1px solid rgba(255, 77, 0, 0.2);
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
padding: 24px;
|
padding: 18px;
|
||||||
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.info-card h3 {
|
/* Гео-чипы в шапке карты */
|
||||||
font-size: 20px;
|
.geo-chips {
|
||||||
font-weight: 600;
|
|
||||||
color: var(--text-primary);
|
|
||||||
margin: 0 0 20px 0;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.info-card p {
|
.geo-chip {
|
||||||
margin: 12px 0;
|
font-size: 12px;
|
||||||
line-height: 1.8;
|
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);
|
color: var(--text-secondary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.info-card p strong {
|
.map-info .sep { color: rgba(255, 77, 0, 0.5); }
|
||||||
color: var(--text-primary);
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-card hr {
|
/* ===== Зоны ===== */
|
||||||
margin: 16px 0;
|
.zones-count {
|
||||||
border: none;
|
font-size: 13px;
|
||||||
border-top: 1px solid rgba(255, 77, 0, 0.2);
|
color: var(--text-secondary);
|
||||||
}
|
|
||||||
|
|
||||||
/* 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-list {
|
.zones-list {
|
||||||
display: grid;
|
display: flex;
|
||||||
gap: 16px;
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
max-height: 560px;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding-right: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.zone-card {
|
.zone-card {
|
||||||
background: rgba(255, 77, 0, 0.05);
|
background: rgba(255, 77, 0, 0.04);
|
||||||
border-radius: 10px;
|
border-radius: 8px;
|
||||||
padding: 20px;
|
padding: 12px 14px;
|
||||||
border-left: 4px solid;
|
border-left: 3px solid var(--border);
|
||||||
transition: all 0.2s ease;
|
transition: background 0.15s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.zone-card:hover {
|
.zone-card:hover {
|
||||||
background: rgba(255, 77, 0, 0.08);
|
background: rgba(255, 77, 0, 0.08);
|
||||||
transform: translateX(4px);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.zone-card.priority-1 {
|
.zone-card.priority-1 {
|
||||||
border-left-color: #ff4d00;
|
border-left-color: #ff4d00;
|
||||||
background: rgba(255, 77, 0, 0.1);
|
background: rgba(255, 77, 0, 0.09);
|
||||||
}
|
}
|
||||||
|
|
||||||
.zone-card.priority-2 {
|
.zone-card.priority-2 {
|
||||||
@@ -216,95 +298,308 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin-bottom: 12px;
|
gap: 10px;
|
||||||
gap: 16px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.zone-title {
|
.zone-title {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 12px;
|
gap: 10px;
|
||||||
flex: 1;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.zone-priority {
|
.zone-priority {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
width: 32px;
|
width: 26px;
|
||||||
height: 32px;
|
height: 26px;
|
||||||
background: var(--accent);
|
background: var(--accent);
|
||||||
color: var(--bg-primary);
|
color: var(--bg-primary);
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
font-size: 16px;
|
font-size: 13px;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.zone-name {
|
.zone-name {
|
||||||
font-size: 18px;
|
font-size: 15px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
|
||||||
.zone-score {
|
/* Направление / дистанция / score — справа компактно */
|
||||||
font-size: 28px;
|
.zone-meta {
|
||||||
font-weight: 700;
|
display: flex;
|
||||||
color: var(--accent);
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.zone-details {
|
.zone-dir {
|
||||||
display: flex;
|
font-size: 12px;
|
||||||
flex-wrap: wrap;
|
font-weight: 700;
|
||||||
gap: 16px;
|
|
||||||
margin-bottom: 12px;
|
|
||||||
font-size: 14px;
|
|
||||||
color: var(--text-secondary);
|
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 {
|
.zone-dist {
|
||||||
color: rgba(255, 77, 0, 0.5);
|
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 {
|
.zone-reasoning {
|
||||||
padding: 12px 16px;
|
margin-top: 8px;
|
||||||
background: rgba(255, 77, 0, 0.08);
|
font-size: 12.5px;
|
||||||
border-radius: 6px;
|
color: var(--text-secondary);
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.zones-empty {
|
||||||
|
padding: 24px;
|
||||||
|
text-align: center;
|
||||||
|
color: var(--text-secondary);
|
||||||
font-size: 14px;
|
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);
|
color: var(--text-secondary);
|
||||||
line-height: 1.6;
|
line-height: 1.6;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Map Section */
|
.unmodeled-note p { margin: 2px 0; }
|
||||||
.map-section {
|
|
||||||
background: var(--bg-secondary);
|
/* Немедленные действия — нумерованный список с акцентом */
|
||||||
border: 1px solid rgba(255, 77, 0, 0.2);
|
.actions-list {
|
||||||
border-radius: 12px;
|
margin: 12px 0 0 0;
|
||||||
padding: 24px;
|
padding: 0;
|
||||||
|
list-style: none;
|
||||||
|
counter-reset: action;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.map-section h2 {
|
.actions-list li {
|
||||||
font-size: 24px;
|
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;
|
font-weight: 600;
|
||||||
color: var(--text-primary);
|
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 {
|
/* ===== Ряд 4: справочные данные ===== */
|
||||||
margin-top: 16px;
|
.reference-grid {
|
||||||
padding: 16px;
|
display: grid;
|
||||||
background: rgba(255, 77, 0, 0.05);
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
border-radius: 8px;
|
gap: 20px;
|
||||||
font-size: 14px;
|
align-items: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-card p {
|
||||||
|
margin: 8px 0;
|
||||||
|
font-size: 13.5px;
|
||||||
|
line-height: 1.6;
|
||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.map-info p {
|
.info-card p strong {
|
||||||
margin: 4px 0;
|
color: var(--text-primary);
|
||||||
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Loading & Error States */
|
/* ===== Loading & Error ===== */
|
||||||
.loading-state,
|
.loading-state,
|
||||||
.error-state {
|
.error-state {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -354,45 +649,34 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.error-state button:hover {
|
.error-state button:hover {
|
||||||
background: #ff6a1a;
|
background: var(--accent-hover);
|
||||||
transform: translateY(-2px);
|
transform: translateY(-2px);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Responsive */
|
/* ===== Responsive ===== */
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 1100px) {
|
||||||
.result-header {
|
.main-row.with-map {
|
||||||
padding: 24px 16px;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
|
.behavior-row {
|
||||||
.result-header h1 {
|
grid-template-columns: 1fr;
|
||||||
font-size: 28px;
|
|
||||||
}
|
}
|
||||||
|
.reference-grid {
|
||||||
.result-content {
|
grid-template-columns: 1fr 1fr;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@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; }
|
||||||
|
}
|
||||||
@@ -5,10 +5,12 @@ import { apiFetch } from "../../api/client";
|
|||||||
import "./AnalysisResult.css";
|
import "./AnalysisResult.css";
|
||||||
|
|
||||||
const GENDER_LABEL = { m: 'М', f: 'Ж' };
|
const GENDER_LABEL = { m: 'М', f: 'Ж' };
|
||||||
|
const DIRECTION_RU = { N: 'С', NE: 'СВ', E: 'В', SE: 'ЮВ', S: 'Ю', SW: 'ЮЗ', W: 'З', NW: 'СЗ' };
|
||||||
|
|
||||||
// Маппинг уровня срочности бэкенда → CSS-класс (фронтовые классы в Cyrillic)
|
// Маппинг уровня срочности бэкенда → CSS-класс (фронтовые классы в Cyrillic)
|
||||||
const urgencyClass = (urgency) => {
|
const urgencyClass = (urgency) => {
|
||||||
const u = (urgency || '').toLowerCase();
|
const u = (urgency || '').toLowerCase();
|
||||||
|
if (u.includes('крит')) return 'критический';
|
||||||
if (u.includes('высок')) return 'высокий';
|
if (u.includes('высок')) return 'высокий';
|
||||||
if (u.includes('сред')) return 'средний';
|
if (u.includes('сред')) return 'средний';
|
||||||
return 'стандартный';
|
return 'стандартный';
|
||||||
@@ -119,228 +121,246 @@ const AnalysisResult = () => {
|
|||||||
const urgencyLevel = urgencyClass(analysisData?.urgency);
|
const urgencyLevel = urgencyClass(analysisData?.urgency);
|
||||||
const profiles = analysisData?.active_profiles || [];
|
const profiles = analysisData?.active_profiles || [];
|
||||||
const warnings = analysisData?.critical_warnings || [];
|
const warnings = analysisData?.critical_warnings || [];
|
||||||
|
const unmodeled = analysisData?.unmodeled_profiles || [];
|
||||||
const tnpLat = caseData.tnp_lat || caseData.gps_lat;
|
const tnpLat = caseData.tnp_lat || caseData.gps_lat;
|
||||||
const tnpLon = caseData.tnp_lon || caseData.gps_lon;
|
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 (
|
return (
|
||||||
<div className="analysis-result">
|
<div className="analysis-result">
|
||||||
|
{/* ===== Шапка: кейс + срочность + радиус ===== */}
|
||||||
<div className="result-header">
|
<div className="result-header">
|
||||||
<h1><span className="logo">ВЕКТОР</span> Результаты анализа</h1>
|
<div className="header-top">
|
||||||
<div className="case-info">
|
<h1><span className="logo">ВЕКТОР</span> Результаты анализа</h1>
|
||||||
<span>{caseData.child_name || 'Ребёнок'}, {caseData.age_years} лет{genderLabel ? ` (${genderLabel})` : ''}</span>
|
<div className="case-info">
|
||||||
<span className="separator">•</span>
|
<span>{caseData.child_name || 'Ребёнок'}, {caseData.age_years} лет{genderLabel ? ` (${genderLabel})` : ''}</span>
|
||||||
<span>Время потери: {caseData.loss_time || 'не указано'}</span>
|
<span className="separator">•</span>
|
||||||
<span className="separator">•</span>
|
<span>Время потери: {caseData.loss_time || 'не указано'}</span>
|
||||||
<span>Прошло: {caseData.elapsed_hours || 1} ч</span>
|
<span className="separator">•</span>
|
||||||
{analysisData && (
|
<span>Прошло: {caseData.elapsed_hours || 1} ч</span>
|
||||||
<>
|
{analysisData && (
|
||||||
<span className="separator">•</span>
|
<>
|
||||||
<span style={{ color: analysisData.fallback_used ? '#8b949e' : '#34c759' }}>
|
<span className="separator">•</span>
|
||||||
{analysisData.fallback_used ? 'Локальный расчёт' : 'ИИ-анализ'}
|
<span style={{ color: analysisData.fallback_used ? 'var(--text-secondary)' : 'var(--success)' }}>
|
||||||
</span>
|
{analysisData.fallback_used ? 'Локальный расчёт' : 'ИИ-анализ'}
|
||||||
</>
|
</span>
|
||||||
)}
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Панель решения: срочность | радиус | действия */}
|
||||||
|
<div className="decision-strip">
|
||||||
|
<div className={`decision-cell urgency-cell urgency-${urgencyLevel}`}>
|
||||||
|
<div className="decision-label">Срочность</div>
|
||||||
|
<div className="decision-value">{analysisData?.urgency || '—'}</div>
|
||||||
|
</div>
|
||||||
|
<div className="decision-cell">
|
||||||
|
<div className="decision-label">Радиус поиска</div>
|
||||||
|
<div className="decision-value radius">
|
||||||
|
{analysisData?.max_distance_km} км
|
||||||
|
{analysisData?.distance_multiplier !== 1 && (
|
||||||
|
<span className="radius-mult">×{analysisData?.distance_multiplier}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="decision-cell warnings-cell">
|
||||||
|
<div className="decision-label">Критические действия</div>
|
||||||
|
{warnings.length > 0 ? (
|
||||||
|
<ul className="decision-warnings">
|
||||||
|
{warnings.map((w, i) => <li key={i}>{w.warning}</li>)}
|
||||||
|
</ul>
|
||||||
|
) : (
|
||||||
|
<div className="decision-warnings muted">Нет критических предупреждений</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{analysisData?.summary && <p className="header-summary">{analysisData.summary}</p>}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="result-content">
|
<div className="result-content">
|
||||||
{/* Блок 1: Уровень срочности */}
|
|
||||||
{analysisData?.urgency && (
|
|
||||||
<div className={`urgency-block urgency-${urgencyLevel}`}>
|
|
||||||
<div className="urgency-level">{analysisData.urgency}</div>
|
|
||||||
<div className="urgency-reason">{analysisData.summary || ''}</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Блок 2: Расчёт по алгоритму Экстремум */}
|
{/* ===== Ряд 1: Карта + зоны ===== */}
|
||||||
<div className="info-card">
|
<div className={`main-row ${hasMapClass(hasMap)}`}>
|
||||||
<h3>📏 Расчёт по алгоритму Экстремум</h3>
|
|
||||||
<div className="distance-info">
|
|
||||||
<div className="distance-value">{analysisData?.max_distance_km} км</div>
|
|
||||||
<div className="distance-details">
|
|
||||||
<p><strong>Формула:</strong> Distance = Time × НормС × СП × СКД × СУ × СУТ × ВВС × ВП</p>
|
|
||||||
<hr style={{ margin: '8px 0', border: 'none', borderTop: '1px solid #30363d' }} />
|
|
||||||
<p><strong>Время (Time):</strong> {caseData.elapsed_hours || 1} ч</p>
|
|
||||||
<p><strong>Базовая скорость (НормС):</strong> {coeffs.base_speed} км/ч (возраст {caseData.age_years} лет)</p>
|
|
||||||
<p><strong>Местность (СП):</strong> {coeffs.terrain} ({caseData.terrain?.join(', ') || 'не указана'})</p>
|
|
||||||
<p><strong>Диагноз (СКД):</strong> {coeffs.diagnosis} {caseData.has_diagnosis ? `(${caseData.diagnosis_type?.join(', ')})` : '(нет)'}</p>
|
|
||||||
<p><strong>Транспорт (СУ):</strong> {coeffs.urgency} {caseData.has_transport && caseData.has_transport !== 'none' ? `(${caseData.has_transport})` : '(нет)'}</p>
|
|
||||||
<p><strong>Усталость (СУТ):</strong> {coeffs.fatigue?.toFixed(2)} (снижение 5%/час)</p>
|
|
||||||
<p><strong>Время суток (ВВС):</strong> {coeffs.time_of_day} ({analysisData?.time_of_day})</p>
|
|
||||||
<p><strong>Погода (ВП):</strong> {coeffs.weather} ({caseData.weather || caseData.precipitation || 'нет'})</p>
|
|
||||||
<hr style={{ margin: '8px 0', border: 'none', borderTop: '1px solid #30363d' }} />
|
|
||||||
<p><strong>Радиус поиска:</strong> {analysisData?.search_radius_km} км × {analysisData?.distance_multiplier} (профили)</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Блок 3: Активные профили и критические предупреждения */}
|
|
||||||
{(profiles.length > 0 || warnings.length > 0) && (
|
|
||||||
<div className="info-card">
|
|
||||||
<h3>⚠️ Активные профили</h3>
|
|
||||||
{profiles.map((p, i) => (
|
|
||||||
<div key={i} style={{ marginBottom: '10px', padding: '10px', background: '#161b22', borderRadius: '6px' }}>
|
|
||||||
<p style={{ margin: '0 0 4px 0' }}><strong>{p.name}</strong></p>
|
|
||||||
{p.note && <p style={{ fontSize: '13px', color: '#8b949e', margin: '2px 0' }}>{p.note}</p>}
|
|
||||||
{p.critical_warning && (
|
|
||||||
<p style={{ fontSize: '13px', color: '#ff4d00', margin: '4px 0 0 0' }}>🚨 {p.critical_warning}</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Блок 4: Психотип и модель поведения */}
|
|
||||||
{analysisData?.psychotype && psyMods && (
|
|
||||||
<div className="info-card">
|
|
||||||
<h3>🧠 Психотип и модель поведения</h3>
|
|
||||||
<p><strong>Психотип:</strong> {analysisData.psychotype}</p>
|
|
||||||
<p><strong>Модель движения:</strong> {psyMods.description}</p>
|
|
||||||
<div style={{ marginTop: '12px', padding: '12px', background: '#161b22', borderRadius: '6px' }}>
|
|
||||||
<p style={{ fontSize: '14px', color: '#8b949e', margin: 0 }}><strong>Модификаторы зон:</strong></p>
|
|
||||||
<p style={{ fontSize: '13px', color: '#8b949e', margin: '4px 0 0 0' }}>
|
|
||||||
• 0-500м: ×{psyMods.zone_0_500}<br />
|
|
||||||
• 500-1500м: ×{psyMods.zone_500_1500}<br />
|
|
||||||
• 1500-2500м: ×{psyMods.zone_1500_2500}<br />
|
|
||||||
• 2500м+: ×{psyMods.zone_2500plus}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
{psyRec && (
|
|
||||||
<div style={{ marginTop: '10px' }}>
|
|
||||||
<p style={{ fontSize: '14px', color: '#8b949e', margin: '0 0 4px 0' }}><strong>Тактика поиска:</strong></p>
|
|
||||||
<p style={{ fontSize: '13px', margin: '2px 0' }}><strong>Приоритетные зоны:</strong> {psyRec.priority_zones}</p>
|
|
||||||
<p style={{ fontSize: '13px', margin: '2px 0' }}><strong>Патрун:</strong> {psyRec.search_pattern}</p>
|
|
||||||
<p style={{ fontSize: '13px', margin: '2px 0' }}><strong>Ключевые объекты:</strong> {psyRec.key_locations}</p>
|
|
||||||
<p style={{ fontSize: '13px', margin: '2px 0' }}><strong>Связь:</strong> {psyRec.communication}</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Блок 5: Обстоятельства потери */}
|
|
||||||
<div className="info-card">
|
|
||||||
<h3>📋 Обстоятельства потери</h3>
|
|
||||||
<p><strong>Причина:</strong> {caseData.reason || caseData.loss_reason || 'не указана'}</p>
|
|
||||||
<p><strong>Последнее известное место:</strong> {caseData.last_known_place || 'не указано'}</p>
|
|
||||||
{caseData.direction && <p><strong>Направление движения:</strong> {caseData.direction}</p>}
|
|
||||||
{caseData.behavior_description && (
|
|
||||||
<p><strong>Описание:</strong> {caseData.behavior_description}</p>
|
|
||||||
)}
|
|
||||||
{analysisData?.behavioral_prediction && (
|
|
||||||
<p><strong>Поведенческий прогноз:</strong> {analysisData.behavioral_prediction}</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Блок 6: Зоны поиска */}
|
|
||||||
<div className="zones-section">
|
|
||||||
<h2>Приоритетные зоны поиска</h2>
|
|
||||||
<div className="zones-list">
|
|
||||||
{zones.slice(0, 8).map((zone, idx) => (
|
|
||||||
<div key={idx} className={`zone-card priority-${Math.min(idx + 1, 3)}`}>
|
|
||||||
<div className="zone-header">
|
|
||||||
<div className="zone-title">
|
|
||||||
<span className="zone-priority">#{idx + 1}</span>
|
|
||||||
<span className="zone-name">{zone.name}</span>
|
|
||||||
</div>
|
|
||||||
<div className="zone-score">{zone.score}%</div>
|
|
||||||
</div>
|
|
||||||
<div className="zone-details">
|
|
||||||
<span><strong>Направление:</strong> {zone.direction}</span>
|
|
||||||
<span className="separator">•</span>
|
|
||||||
<span><strong>Дистанция:</strong> {zone.distance}</span>
|
|
||||||
</div>
|
|
||||||
<div className="zone-reasoning">{zone.reasoning}</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Блок 7: Немедленные действия */}
|
|
||||||
{analysisData?.immediate_actions?.length > 0 && (
|
|
||||||
<div className="info-card">
|
|
||||||
<h3>🚑 Немедленные действия</h3>
|
|
||||||
<ul style={{ margin: 0, paddingLeft: '20px' }}>
|
|
||||||
{analysisData.immediate_actions.map((a, i) => (
|
|
||||||
<li key={i} style={{ marginBottom: '4px' }}>{a}</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Блок 8: Карта */}
|
|
||||||
{tnpLat && tnpLon && (
|
|
||||||
<div className="map-section">
|
<div className="map-section">
|
||||||
<h2>Карта зон поиска</h2>
|
<div className="section-head">
|
||||||
|
<h2>Карта зон поиска</h2>
|
||||||
|
{(waterData?.count > 0) && (
|
||||||
|
<div className="geo-chips">
|
||||||
|
<span className="geo-chip water">💧 Водоёмы: {waterCount}</span>
|
||||||
|
{railCount > 0 && <span className="geo-chip rail">🚂 Ж/д: {railCount}</span>}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
{waterData?.warning && (
|
{waterData?.warning && (
|
||||||
<div style={{ marginBottom: '10px', padding: '10px', background: '#2d1b0e', border: '1px solid #ff4d00', borderRadius: '6px' }}>
|
<div className="geo-warning">
|
||||||
<p style={{ margin: 0, fontSize: '13px', color: '#ff4d00' }}>
|
🚨 {waterData.warning}
|
||||||
🚨 {waterData.warning}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{waterData?.railway_warning && (waterData.features || []).some((f) => f.layer === 'railway') && (
|
{waterData?.railway_warning && railCount > 0 && (
|
||||||
<div style={{ marginBottom: '10px', padding: '10px', background: '#2d1b0e', border: '1px solid #8a2f2f', borderRadius: '6px' }}>
|
<div className="geo-warning rail">
|
||||||
<p style={{ margin: 0, fontSize: '13px', color: '#ff7b72' }}>
|
🚂 {waterData.railway_warning}
|
||||||
🚂 {waterData.railway_warning}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{waterData?.count > 0 && (
|
{hasMap ? (
|
||||||
<p style={{ margin: '0 0 8px 0', fontSize: '13px', color: '#8b949e' }}>
|
<SearchMap zones={zones}
|
||||||
💧 Водоёмы в зоне поиска: {waterData.features.filter(f => f.layer !== 'railway').length}
|
maxDistance={parseFloat(analysisData?.max_distance_km)}
|
||||||
{' '}· 🚂 Ж/д линий: {waterData.features.filter(f => f.layer === 'railway').length}
|
tnpLat={parseFloat(tnpLat)}
|
||||||
{waterData.warning && ' — приоритетная проверка'} (данные OSM)
|
tnpLon={parseFloat(tnpLon)}
|
||||||
</p>
|
waterFeatures={waterData?.features || []}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="map-empty">ТНП не задана — карта недоступна</div>
|
||||||
)}
|
)}
|
||||||
<SearchMap zones={zones}
|
<div className="map-info">
|
||||||
maxDistance={parseFloat(analysisData?.max_distance_km)}
|
<span>ТНП: {tnpLat ? Number(tnpLat).toFixed(5) : '—'}, {tnpLon ? Number(tnpLon).toFixed(5) : '—'}</span>
|
||||||
tnpLat={parseFloat(tnpLat)}
|
{caseData.tnp_address && <span className="sep">·</span>}
|
||||||
tnpLon={parseFloat(tnpLon)}
|
{caseData.tnp_address && <span>{caseData.tnp_address}</span>}
|
||||||
waterFeatures={waterData?.features || []}
|
|
||||||
/>
|
|
||||||
<div className="map-info" style={{ marginTop: "12px", fontSize: "14px", color: "#8b949e" }}>
|
|
||||||
<p>Координаты ТНП: {tnpLat}, {tnpLon}</p>
|
|
||||||
{caseData.tnp_address && <p>Адрес: {caseData.tnp_address}</p>}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Блок 9: Здоровье и особенности */}
|
<div className="zones-panel">
|
||||||
{(caseData.has_diagnosis || caseData.cant_swim || caseData.fitness_level || caseData.has_transport) && (
|
<div className="section-head">
|
||||||
<div className="info-card">
|
<h2>Зоны поиска</h2>
|
||||||
<h3>🏥 Здоровье и особенности</h3>
|
<span className="zones-count">{zones.length} зон</span>
|
||||||
{caseData.has_diagnosis && (
|
</div>
|
||||||
<p><strong>Диагнозы:</strong> {caseData.diagnosis_type?.join(', ') || 'есть'}</p>
|
<div className="zones-list">
|
||||||
|
{zones.slice(0, 8).map((zone, idx) => (
|
||||||
|
<div key={idx} className={`zone-card priority-${Math.min(idx + 1, 3)}`}>
|
||||||
|
<div className="zone-header">
|
||||||
|
<div className="zone-title">
|
||||||
|
<span className="zone-priority">{idx + 1}</span>
|
||||||
|
<span className="zone-name">{zone.name}</span>
|
||||||
|
</div>
|
||||||
|
<div className="zone-meta">
|
||||||
|
<span className="zone-dir">{DIRECTION_RU[zone.direction] || zone.direction}</span>
|
||||||
|
<span className="zone-dist">{zone.distance}</span>
|
||||||
|
<div className="zone-score">{zone.score}%</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{zone.reasoning && <div className="zone-reasoning">{zone.reasoning}</div>}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
{zones.length === 0 && <div className="zones-empty">Зоны не рассчитаны</div>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* ===== Ряд 2: Поведение + действия ===== */}
|
||||||
|
<div className="behavior-row">
|
||||||
|
<div className="info-card behavior-card">
|
||||||
|
<div className="section-head">
|
||||||
|
<h3>Поведение и профиль</h3>
|
||||||
|
{(profiles.length > 0 || unmodeled.length > 0) && (
|
||||||
|
<div className="profile-tags">
|
||||||
|
{profiles.map((p, i) => <span key={i} className="profile-tag">{p.name}</span>)}
|
||||||
|
{unmodeled.map((u, i) => <span key={`u${i}`} className="profile-tag unmodeled" title={u.note}>{u.profile}*</span>)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{analysisData?.behavioral_prediction && (
|
||||||
|
<p className="prediction">{analysisData.behavioral_prediction}</p>
|
||||||
)}
|
)}
|
||||||
{caseData.fitness_level && (
|
{psyRec && (
|
||||||
<p><strong>Физическая подготовка:</strong> {caseData.fitness_level}</p>
|
<div className="tactics-grid">
|
||||||
|
<div className="tactic"><div className="tactic-label">Приоритетные зоны</div><div>{psyRec.priority_zones}</div></div>
|
||||||
|
<div className="tactic"><div className="tactic-label">Паттерн поиска</div><div>{psyRec.search_pattern}</div></div>
|
||||||
|
<div className="tactic"><div className="tactic-label">Ключевые объекты</div><div>{psyRec.key_locations}</div></div>
|
||||||
|
<div className="tactic"><div className="tactic-label">Связь</div><div>{psyRec.communication}</div></div>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
{caseData.cant_swim && <p>⚠️ <strong>Не умеет плавать</strong> — приоритет ближайшим водоёмам</p>}
|
{unmodeled.length > 0 && (
|
||||||
{caseData.has_transport && caseData.has_transport !== 'none' && (
|
<div className="unmodeled-note">
|
||||||
<p><strong>Транспорт:</strong> {caseData.has_transport}</p>
|
{unmodeled.map((u, i) => (
|
||||||
|
<p key={i}>• {u.profile}: {u.note}</p>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
{(analysisData?.unmodeled_profiles || []).length > 0 && (
|
</div>
|
||||||
<div style={{ marginTop: '10px', padding: '10px', background: '#161b22', borderRadius: '6px' }}>
|
|
||||||
<p style={{ fontSize: '13px', color: '#8b949e', margin: '0 0 4px 0' }}>
|
<div className="info-card actions-card">
|
||||||
<strong>Вне поведенческой модели</strong> (на скоринг зон не влияют):
|
<h3>Немедленные действия</h3>
|
||||||
</p>
|
<ol className="actions-list">
|
||||||
{analysisData.unmodeled_profiles.map((u, i) => (
|
{(analysisData?.immediate_actions || []).map((a, i) => (
|
||||||
<p key={i} style={{ fontSize: '13px', color: '#8b949e', margin: '2px 0' }}>
|
<li key={i} className={a.startsWith('КРИТИЧНО') ? 'critical' : ''}>{a}</li>
|
||||||
• {u.profile}: {u.note}
|
))}
|
||||||
</p>
|
</ol>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* ===== Ряд 3: Обоснование радиуса (формула Экстремум) ===== */}
|
||||||
|
{analysisData && (
|
||||||
|
<div className="info-card radius-card">
|
||||||
|
<div className="section-head">
|
||||||
|
<h3>Обоснование радиуса</h3>
|
||||||
|
<span className="radius-formula">Distance = Time × НормС × СП × СКД × СУ × СУТ × ВВС × ВП</span>
|
||||||
|
</div>
|
||||||
|
<div className="coeff-grid">
|
||||||
|
{coeffChips.map((c, i) => (
|
||||||
|
<div key={i} className="coeff-chip">
|
||||||
|
<div className="coeff-name">{c.label} <span className="coeff-value">{c.value}</span></div>
|
||||||
|
<div className="coeff-hint">{c.hint}</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
{psyMods && (
|
||||||
|
<div className="psy-mods">
|
||||||
|
<span className="psy-mods-title">Психотип {psychoRu(analysisData?.psychotype)} — поправки полос:</span>
|
||||||
|
{['0–500 м', '500–1500 м', '1500–2500 м', '2500 м+'].map((band, i) => (
|
||||||
|
<span key={i} className="psy-mod-chip">{band}: ×{[psyMods.zone_0_500, psyMods.zone_500_1500, psyMods.zone_1500_2500, psyMods.zone_2500plus][i]}</span>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Блок 10: Условия среды */}
|
{/* ===== Ряд 4: Справочные данные (3 колонки) ===== */}
|
||||||
<div className="info-card">
|
<div className="reference-grid">
|
||||||
<h3>🌤 Условия среды</h3>
|
<div className="info-card">
|
||||||
<p><strong>Сезон:</strong> {caseData.season || 'не указан'}</p>
|
<h3>Обстоятельства потери</h3>
|
||||||
{caseData.temperature_c != null && <p><strong>Температура:</strong> {caseData.temperature_c}°C</p>}
|
<p><strong>Причина:</strong> {caseData.reason || caseData.loss_reason || 'не указана'}</p>
|
||||||
<p><strong>Осадки:</strong> {caseData.weather || caseData.precipitation || 'нет'}</p>
|
<p><strong>Последнее известное место:</strong> {caseData.last_known_place || 'не указано'}</p>
|
||||||
{caseData.visibility && <p><strong>Видимость:</strong> {caseData.visibility}</p>}
|
{caseData.direction && <p><strong>Направление движения:</strong> {caseData.direction}</p>}
|
||||||
|
{caseData.behavior_description && <p><strong>Описание:</strong> {caseData.behavior_description}</p>}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="info-card">
|
||||||
|
<h3>Здоровье и особенности</h3>
|
||||||
|
{caseData.has_diagnosis ? (
|
||||||
|
<p><strong>Диагнозы:</strong> {caseData.diagnosis_type?.join(', ')}</p>
|
||||||
|
) : (
|
||||||
|
<p><strong>Диагнозы:</strong> нет</p>
|
||||||
|
)}
|
||||||
|
{caseData.fitness_level && <p><strong>Физподготовка:</strong> {caseData.fitness_level}</p>}
|
||||||
|
{caseData.cant_swim && <p>⚠️ <strong>Не умеет плавать</strong> — приоритет ближайшим водоёмам</p>}
|
||||||
|
{caseData.has_transport && caseData.has_transport !== 'none' && (
|
||||||
|
<p><strong>Транспорт:</strong> {caseData.has_transport}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="info-card">
|
||||||
|
<h3>Условия среды</h3>
|
||||||
|
<p><strong>Сезон:</strong> {caseData.season || 'не указан'}</p>
|
||||||
|
{caseData.temperature_c != null && <p><strong>Температура:</strong> {caseData.temperature_c}°C</p>}
|
||||||
|
<p><strong>Осадки:</strong> {caseData.weather || caseData.precipitation || 'нет'}</p>
|
||||||
|
{caseData.visibility && <p><strong>Видимость:</strong> {caseData.visibility}</p>}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
@@ -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;
|
||||||
Reference in New Issue
Block a user