Import Vector lab project
This commit is contained in:
@@ -0,0 +1,398 @@
|
||||
.analysis-result {
|
||||
min-height: 100vh;
|
||||
background: var(--bg-primary);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
/* 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;
|
||||
}
|
||||
|
||||
.result-header h1 {
|
||||
font-size: 36px;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
margin: 0 0 16px 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.logo {
|
||||
color: var(--accent);
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.case-info {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 16px;
|
||||
font-size: 16px;
|
||||
color: var(--text-secondary);
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.separator {
|
||||
color: rgba(255, 77, 0, 0.5);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
/* Content */
|
||||
.result-content {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
padding: 0 24px 48px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
/* Urgency Block */
|
||||
.urgency-block {
|
||||
padding: 24px;
|
||||
border-radius: 12px;
|
||||
border: 2px solid;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.urgency-block.urgency-высокий {
|
||||
background: rgba(255, 77, 0, 0.15);
|
||||
border-color: #ff4d00;
|
||||
}
|
||||
|
||||
.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 */
|
||||
.info-card {
|
||||
background: var(--bg-secondary);
|
||||
border: 1px solid rgba(255, 77, 0, 0.2);
|
||||
border-radius: 12px;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.info-card h3 {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
margin: 0 0 20px 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.info-card p {
|
||||
margin: 12px 0;
|
||||
line-height: 1.8;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.info-card p strong {
|
||||
color: var(--text-primary);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.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-list {
|
||||
display: grid;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.zone-card {
|
||||
background: rgba(255, 77, 0, 0.05);
|
||||
border-radius: 10px;
|
||||
padding: 20px;
|
||||
border-left: 4px solid;
|
||||
transition: all 0.2s 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);
|
||||
}
|
||||
|
||||
.zone-card.priority-2 {
|
||||
border-left-color: #ff9f1a;
|
||||
}
|
||||
|
||||
.zone-card.priority-3 {
|
||||
border-left-color: #ffd166;
|
||||
}
|
||||
|
||||
.zone-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 12px;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.zone-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.zone-priority {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
background: var(--accent);
|
||||
color: var(--bg-primary);
|
||||
border-radius: 50%;
|
||||
font-weight: 700;
|
||||
font-size: 16px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.zone-name {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.zone-score {
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
color: var(--accent);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.zone-details {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 16px;
|
||||
margin-bottom: 12px;
|
||||
font-size: 14px;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.zone-details .separator {
|
||||
color: rgba(255, 77, 0, 0.5);
|
||||
}
|
||||
|
||||
.zone-reasoning {
|
||||
padding: 12px 16px;
|
||||
background: rgba(255, 77, 0, 0.08);
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
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;
|
||||
}
|
||||
|
||||
.map-section h2 {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
margin: 0 0 20px 0;
|
||||
}
|
||||
|
||||
.map-info {
|
||||
margin-top: 16px;
|
||||
padding: 16px;
|
||||
background: rgba(255, 77, 0, 0.05);
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.map-info p {
|
||||
margin: 4px 0;
|
||||
}
|
||||
|
||||
/* Loading & Error States */
|
||||
.loading-state,
|
||||
.error-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 60vh;
|
||||
gap: 24px;
|
||||
padding: 48px 24px;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border: 4px solid rgba(255, 77, 0, 0.2);
|
||||
border-top-color: var(--accent);
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.error-state h2 {
|
||||
font-size: 24px;
|
||||
color: var(--text-primary);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.error-state p {
|
||||
font-size: 16px;
|
||||
color: var(--text-secondary);
|
||||
margin: 8px 0 24px;
|
||||
}
|
||||
|
||||
.error-state button {
|
||||
padding: 12px 24px;
|
||||
background: var(--accent);
|
||||
color: var(--bg-primary);
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.error-state button:hover {
|
||||
background: #ff6a1a;
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 768px) {
|
||||
.result-header {
|
||||
padding: 24px 16px;
|
||||
}
|
||||
|
||||
.result-header h1 {
|
||||
font-size: 28px;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,414 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { useParams } from "react-router-dom";
|
||||
import SearchMap from '../../components/SearchMap';
|
||||
import "./AnalysisResult.css";
|
||||
|
||||
const AnalysisResult = () => {
|
||||
const { caseId } = useParams();
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState(null);
|
||||
const [caseData, setCaseData] = useState(null);
|
||||
const [analysisData, setAnalysisData] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
const loadCaseAndAnalyze = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
const caseResponse = await fetch(`/api/v1/cases/${caseId}`);
|
||||
if (!caseResponse.ok) {
|
||||
throw new Error("Не удалось загрузить карточку");
|
||||
}
|
||||
const caseResult = await caseResponse.json();
|
||||
setCaseData(caseResult);
|
||||
|
||||
const analysis = calculateFullDistance(caseResult);
|
||||
setAnalysisData(analysis);
|
||||
|
||||
} catch (err) {
|
||||
console.error("Error loading case:", err);
|
||||
setError(err.message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (caseId) {
|
||||
loadCaseAndAnalyze();
|
||||
}
|
||||
}, [caseId]);
|
||||
|
||||
const getTimeOfDay = (lostTime) => {
|
||||
if (!lostTime) return 'день';
|
||||
const hour = parseInt(lostTime.split(':')[0]);
|
||||
if (hour >= 6 && hour < 18) return 'день';
|
||||
if (hour >= 18 && hour < 22) return 'сумерки';
|
||||
return 'ночь';
|
||||
};
|
||||
|
||||
const getPsychotypeModifiers = (psychotype) => {
|
||||
const modifiers = {
|
||||
'dominant': {
|
||||
zone_0_500: 0.7,
|
||||
zone_500_1500: 1.2,
|
||||
zone_1500_2500: 1.4,
|
||||
zone_2500plus: 1.1,
|
||||
description: 'Доминантный: активное движение, большие расстояния'
|
||||
},
|
||||
'harmonic': {
|
||||
zone_0_500: 0.8,
|
||||
zone_500_1500: 1.0,
|
||||
zone_1500_2500: 1.1,
|
||||
zone_2500plus: 0.9,
|
||||
description: 'Гармоничный: рациональное движение по ориентирам'
|
||||
},
|
||||
'anxious': {
|
||||
zone_0_500: 1.4,
|
||||
zone_500_1500: 1.1,
|
||||
zone_1500_2500: 0.5,
|
||||
zone_2500plus: 0.3,
|
||||
description: 'Тревожный: минимальное движение, остается близко'
|
||||
},
|
||||
'introvert_passive': {
|
||||
zone_0_500: 1.3,
|
||||
zone_500_1500: 0.9,
|
||||
zone_1500_2500: 0.6,
|
||||
zone_2500plus: 0.2,
|
||||
description: 'Интроверт пассивный: прячется, минимальное движение'
|
||||
},
|
||||
'introvert_active': {
|
||||
zone_0_500: 0.8,
|
||||
zone_500_1500: 1.1,
|
||||
zone_1500_2500: 1.2,
|
||||
zone_2500plus: 0.9,
|
||||
description: 'Интроверт активный: целенаправленное движение'
|
||||
}
|
||||
};
|
||||
|
||||
return modifiers[psychotype] || modifiers['harmonic'];
|
||||
};
|
||||
|
||||
const calculateFullDistance = (caseData) => {
|
||||
const age = caseData.age_years || 10;
|
||||
const elapsed = caseData.elapsed_hours || 1;
|
||||
const terrain = caseData.terrain?.[0] || 'лес';
|
||||
const timeOfDay = getTimeOfDay(caseData.lost_time);
|
||||
const weather = caseData.precipitation || 'нет';
|
||||
|
||||
// НормС - Скорость смещения потерявшегося ребёнка (км/ч)
|
||||
// Не скорость ходьбы, а реальное смещение: испуг, кружение, остановки
|
||||
// Основа: ПСО ЭКСТРЕМУМ, 400 случаев — 94% найдены в пределах 3 км
|
||||
let baseSpeed = 1.5;
|
||||
if (age <= 2) baseSpeed = 0.3;
|
||||
else if (age <= 5) baseSpeed = 0.7;
|
||||
else if (age <= 8) baseSpeed = 1.2;
|
||||
else if (age <= 12) baseSpeed = 1.5;
|
||||
else if (age <= 15) baseSpeed = 2.0;
|
||||
else if (age <= 17) baseSpeed = 2.5;
|
||||
else if (age <= 64) baseSpeed = 2.5;
|
||||
else baseSpeed = 1.5;
|
||||
|
||||
// СП - Коэффициент местности
|
||||
let terrainCoef = 0.5;
|
||||
const terrainLower = terrain.toLowerCase();
|
||||
if (terrainLower.includes('дорог') || terrainLower.includes('троп')) terrainCoef = 0.8;
|
||||
else if (terrainLower.includes('поле') || terrainLower.includes('луг')) terrainCoef = 0.9;
|
||||
else if (terrainLower.includes('болот')) terrainCoef = 0.2;
|
||||
else if (terrainLower.includes('лес')) terrainCoef = 0.5;
|
||||
else if (terrainLower.includes('населённый')) terrainCoef = 1.0;
|
||||
else if (terrainLower.includes('овраг')) terrainCoef = 0.3;
|
||||
|
||||
// СКД - Коэффициент диагноза (влияние на физическую способность двигаться)
|
||||
let diagnosisCoef = 1.0;
|
||||
const diagTypes = (caseData.diagnosis_type || [])
|
||||
.filter(d => d && d !== 'нет' && d !== 'none')
|
||||
.map(d => d.toLowerCase());
|
||||
if (caseData.has_diagnosis && diagTypes.length > 0) {
|
||||
if (diagTypes.some(d => d.includes('эпилепси'))) diagnosisCoef = 0.6;
|
||||
else if (diagTypes.some(d => d.includes('зпр'))) diagnosisCoef = 0.8;
|
||||
else if (diagTypes.some(d => d.includes('рас') || d.includes('аутизм'))) diagnosisCoef = 1.0;
|
||||
else if (diagTypes.some(d => d.includes('сдвг'))) diagnosisCoef = 1.1;
|
||||
else diagnosisCoef = 0.9;
|
||||
}
|
||||
|
||||
// СУ - Коэффициент транспорта
|
||||
// 'none' — это строка, проверяем явно чтобы не получить false positive
|
||||
let urgencyCoef = 1.0;
|
||||
if (caseData.has_transport && caseData.has_transport !== 'none') {
|
||||
if (caseData.has_transport === 'bike') urgencyCoef = 5.0;
|
||||
else if (caseData.has_transport === 'car') urgencyCoef = 8.0;
|
||||
else urgencyCoef = 3.0;
|
||||
}
|
||||
|
||||
// СУТ - Коэффициент усталости
|
||||
const fatigueCoef = Math.max(0.3, 1.0 - (0.05 * elapsed));
|
||||
|
||||
// ВВС - Коэффициент времени суток
|
||||
let timeCoef = 1.0;
|
||||
if (timeOfDay === 'ночь') timeCoef = 0.5;
|
||||
else if (timeOfDay === 'сумерки') timeCoef = 0.7;
|
||||
|
||||
// ВП - Коэффициент погоды
|
||||
let weatherCoef = 1.0;
|
||||
const weatherLower = weather.toLowerCase();
|
||||
if (weatherLower.includes('дождь')) weatherCoef = 0.8;
|
||||
else if (weatherLower.includes('снег')) weatherCoef = 0.6;
|
||||
else if (weatherLower.includes('туман')) weatherCoef = 0.7;
|
||||
|
||||
// Полная формула
|
||||
const distance = elapsed * baseSpeed * terrainCoef * diagnosisCoef * urgencyCoef * fatigueCoef * timeCoef * weatherCoef;
|
||||
|
||||
// Получаем модификаторы психотипа
|
||||
const psychotypeModifiers = caseData.psychotype ? getPsychotypeModifiers(caseData.psychotype) : null;
|
||||
|
||||
return {
|
||||
max_distance_km: distance.toFixed(2),
|
||||
coefficients: {
|
||||
base_speed: baseSpeed,
|
||||
terrain: terrainCoef,
|
||||
diagnosis: diagnosisCoef,
|
||||
urgency: urgencyCoef,
|
||||
fatigue: fatigueCoef,
|
||||
time_of_day: timeCoef,
|
||||
weather: weatherCoef
|
||||
},
|
||||
time_of_day: timeOfDay,
|
||||
psychotype_modifiers: psychotypeModifiers,
|
||||
zones: generateZonesWithPsychotype(distance, caseData, psychotypeModifiers)
|
||||
};
|
||||
};
|
||||
|
||||
const generateZonesWithPsychotype = (maxDist, caseData, psychotypeModifiers) => {
|
||||
const directions = [
|
||||
{ dir: 'N', name: 'Север' },
|
||||
{ dir: 'NE', name: 'Северо-Восток' },
|
||||
{ dir: 'E', name: 'Восток' },
|
||||
{ dir: 'SE', name: 'Юго-Восток' },
|
||||
{ dir: 'S', name: 'Юг' },
|
||||
{ dir: 'SW', name: 'Юго-Запад' },
|
||||
{ dir: 'W', name: 'Запад' },
|
||||
{ dir: 'NW', name: 'Северо-Запад' }
|
||||
];
|
||||
|
||||
const preferredDir = caseData.direction;
|
||||
|
||||
return directions.map((d, i) => {
|
||||
let score = 100 - (i * 10);
|
||||
|
||||
// Применяем модификатор направления
|
||||
if (preferredDir && d.dir === preferredDir) {
|
||||
score += 30;
|
||||
}
|
||||
|
||||
// Применяем модификатор психотипа в зависимости от дистанции
|
||||
if (psychotypeModifiers) {
|
||||
const distKm = maxDist * 0.75; // средняя дистанция сектора
|
||||
let psychoMod = 1.0;
|
||||
|
||||
if (distKm < 0.5) psychoMod = psychotypeModifiers.zone_0_500;
|
||||
else if (distKm < 1.5) psychoMod = psychotypeModifiers.zone_500_1500;
|
||||
else if (distKm < 2.5) psychoMod = psychotypeModifiers.zone_1500_2500;
|
||||
else psychoMod = psychotypeModifiers.zone_2500plus;
|
||||
|
||||
score = score * psychoMod;
|
||||
}
|
||||
|
||||
let reasoning = 'Расчет по алгоритму Экстремум';
|
||||
if (preferredDir === d.dir) reasoning += ' (указанное направление)';
|
||||
if (psychotypeModifiers) reasoning += ` (психотип учтен)`;
|
||||
|
||||
return {
|
||||
priority: i + 1,
|
||||
name: `Сектор ${d.name}`,
|
||||
direction: d.dir,
|
||||
distance: `${(maxDist * 0.5).toFixed(1)}-${maxDist.toFixed(1)} км`,
|
||||
score: Math.min(100, Math.round(score)),
|
||||
reasoning
|
||||
};
|
||||
}).sort((a, b) => b.score - a.score);
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="analysis-result">
|
||||
<div className="loading-state">
|
||||
<div className="spinner"></div>
|
||||
<p>Загрузка данных и расчет зон поиска...</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="analysis-result">
|
||||
<div className="error-state">
|
||||
<h2>Ошибка загрузки</h2>
|
||||
<p>{error}</p>
|
||||
<button onClick={() => window.location.reload()}>Попробовать снова</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!caseData) {
|
||||
return (
|
||||
<div className="analysis-result">
|
||||
<div className="error-state">
|
||||
<h2>Карточка не найдена</h2>
|
||||
<p>ID: {caseId}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const urgencyLevel = caseData.elapsed_hours > 6 ? "ВЫСОКИЙ" : caseData.elapsed_hours > 3 ? "СРЕДНИЙ" : "СТАНДАРТНЫЙ";
|
||||
const urgencyReason = `Прошло ${caseData.elapsed_hours || 1} часов. ${caseData.has_transport ? `Транспорт: ${caseData.has_transport} - расширенный радиус.` : ''} ${caseData.has_diagnosis ? 'Есть диагноз - требуется особое внимание.' : ''}`;
|
||||
|
||||
const zones = analysisData?.zones || [];
|
||||
const coeffs = analysisData?.coefficients || {};
|
||||
|
||||
return (
|
||||
<div className="analysis-result">
|
||||
<div className="result-header">
|
||||
<h1><span className="logo">ВЕКТОР</span> Результаты анализа</h1>
|
||||
<div className="case-info">
|
||||
<span>{caseData.child_name || 'Ребенок'}, {caseData.age_years} лет ({caseData.gender})</span>
|
||||
<span className="separator">•</span>
|
||||
<span>Время потери: {caseData.lost_time || 'не указано'}</span>
|
||||
<span className="separator">•</span>
|
||||
<span>Прошло: {caseData.elapsed_hours || 1} ч</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="result-content">
|
||||
{/* Блок 1: Уровень срочности */}
|
||||
<div className={`urgency-block urgency-${urgencyLevel.toLowerCase()}`}>
|
||||
<div className="urgency-level">{urgencyLevel}</div>
|
||||
<div className="urgency-reason">{urgencyReason}</div>
|
||||
</div>
|
||||
|
||||
{/* Блок 2: Расчет по алгоритму Экстремум */}
|
||||
<div className="info-card">
|
||||
<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})` : '(нет)'}</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.precipitation || 'нет'})</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Блок 3: Психотип */}
|
||||
{caseData.psychotype && analysisData?.psychotype_modifiers && (
|
||||
<div className="info-card">
|
||||
<h3>🧠 Психотип и модель поведения</h3>
|
||||
<p><strong>Психотип:</strong> {caseData.psychotype}</p>
|
||||
<p><strong>Модель движения:</strong> {analysisData.psychotype_modifiers.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м: ×{analysisData.psychotype_modifiers.zone_0_500}<br/>
|
||||
• 500-1500м: ×{analysisData.psychotype_modifiers.zone_500_1500}<br/>
|
||||
• 1500-2500м: ×{analysisData.psychotype_modifiers.zone_1500_2500}<br/>
|
||||
• 2500м+: ×{analysisData.psychotype_modifiers.zone_2500plus}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Блок 4: Обстоятельства */}
|
||||
<div className="info-card">
|
||||
<h3>📋 Обстоятельства потери</h3>
|
||||
<p><strong>Причина:</strong> {caseData.reason || 'не указана'}</p>
|
||||
<p><strong>Последнее известное место:</strong> {caseData.last_known_place || 'не указано'}</p>
|
||||
{caseData.direction && <p><strong>Направление движения:</strong> {caseData.direction}</p>}
|
||||
{caseData.circumstances_description && (
|
||||
<p><strong>Описание:</strong> {caseData.circumstances_description}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Блок 5: Зоны поиска */}
|
||||
<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>
|
||||
|
||||
{/* Блок 6: Карта */}
|
||||
{caseData.tnp_lat && caseData.tnp_lon && (
|
||||
<div className="map-section">
|
||||
<h2>Карта зон поиска</h2>
|
||||
<SearchMap zones={zones}
|
||||
maxDistance={parseFloat(analysisData?.max_distance_km)}
|
||||
tnpLat={parseFloat(caseData.tnp_lat)}
|
||||
tnpLon={parseFloat(caseData.tnp_lon)}
|
||||
/>
|
||||
<div className="map-info" style={{ marginTop: "12px", fontSize: "14px", color: "#8b949e" }}>
|
||||
<p>Координаты ТНП: {caseData.tnp_lat}, {caseData.tnp_lon}</p>
|
||||
{caseData.tnp_address && <p>Адрес: {caseData.tnp_address}</p>}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Блок 7: Здоровье и особенности */}
|
||||
{(caseData.has_diagnosis || caseData.cant_swim || caseData.fitness_level) && (
|
||||
<div className="info-card">
|
||||
<h3>🏥 Здоровье и особенности</h3>
|
||||
{caseData.has_diagnosis && (
|
||||
<p><strong>Диагнозы:</strong> {caseData.diagnosis_type?.join(', ') || 'есть'}</p>
|
||||
)}
|
||||
{caseData.fitness_level && (
|
||||
<p><strong>Физическая подготовка:</strong> {caseData.fitness_level}</p>
|
||||
)}
|
||||
{caseData.cant_swim && <p>⚠️ <strong>Не умеет плавать</strong></p>}
|
||||
{caseData.has_transport && (
|
||||
<p><strong>Транспорт:</strong> {caseData.has_transport}</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Блок 8: Условия среды */}
|
||||
<div className="info-card">
|
||||
<h3>🌤 Условия среды</h3>
|
||||
<p><strong>Сезон:</strong> {caseData.season || 'не указан'}</p>
|
||||
{caseData.temperature && <p><strong>Температура:</strong> {caseData.temperature}°C</p>}
|
||||
<p><strong>Осадки:</strong> {caseData.precipitation || 'нет'}</p>
|
||||
{caseData.visibility && <p><strong>Видимость:</strong> {caseData.visibility}</p>}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AnalysisResult;
|
||||
Reference in New Issue
Block a user