Merge branch 'AEB-71/navigation_and_dashboard_update_1' into 'main'
Added more detectors to DB See merge request wedeving/aerbim-www!14
This commit is contained in:
@@ -95,11 +95,18 @@ const NavigationPage: React.FC = () => {
|
||||
}, [])
|
||||
|
||||
const handleModelError = useCallback((error: string) => {
|
||||
console.error('Model loading error:', error)
|
||||
console.error('[NavigationPage] Model loading error:', error)
|
||||
setModelError(error)
|
||||
setIsModelReady(false)
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedModelPath) {
|
||||
setIsModelReady(false);
|
||||
setModelError(null);
|
||||
}
|
||||
}, [selectedModelPath]);
|
||||
|
||||
useEffect(() => {
|
||||
if (urlObjectId && urlObjectTitle && (!currentObject.id || currentObject.id !== urlObjectId)) {
|
||||
setCurrentObject(urlObjectId, urlObjectTitle)
|
||||
@@ -206,6 +213,7 @@ const NavigationPage: React.FC = () => {
|
||||
objectId={objectId || undefined}
|
||||
onClose={closeMonitoring}
|
||||
onSelectModel={(path) => {
|
||||
console.log('[NavigationPage] Model selected:', path);
|
||||
setSelectedModelPath(path)
|
||||
setModelError(null)
|
||||
setIsModelReady(false)
|
||||
@@ -291,6 +299,8 @@ const NavigationPage: React.FC = () => {
|
||||
<div className="flex-1 overflow-hidden">
|
||||
<div className="h-full">
|
||||
{modelError ? (
|
||||
<>
|
||||
{console.log('[NavigationPage] Rendering error message, modelError:', modelError)}
|
||||
<div className="h-full flex items-center justify-center bg-[#0e111a]">
|
||||
<div className="text-center p-8 bg-[#161824] rounded-lg border border-gray-700 max-w-md">
|
||||
<div className="text-red-400 text-lg font-semibold mb-4">
|
||||
@@ -304,6 +314,21 @@ const NavigationPage: React.FC = () => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
) : !selectedModelPath ? (
|
||||
<div className="h-full flex items-center justify-center bg-[#0e111a]">
|
||||
<div className="text-center p-8 bg-[#161824] rounded-lg border border-gray-700 max-w-md">
|
||||
<div className="text-gray-400 text-lg font-semibold mb-4">
|
||||
3D модель не загружена
|
||||
</div>
|
||||
<div className="text-gray-300 mb-4">
|
||||
Модель не готова к отображению
|
||||
</div>
|
||||
<div className="text-sm text-gray-400">
|
||||
Выберите модель из навигации по этажам
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<ModelViewer
|
||||
modelPath={selectedModelPath}
|
||||
|
||||
@@ -28,8 +28,7 @@ export async function GET() {
|
||||
title = title.replace(/\bprop\b/gi, '');
|
||||
title = title.replace(/\s{2,}/g, ' ').trim();
|
||||
|
||||
const pathUrl = `/static-models/${filename}`;
|
||||
return { name: title, path: pathUrl };
|
||||
return { name: title, path: `/static-models/${filename}` };
|
||||
});
|
||||
|
||||
return new Response(JSON.stringify({ models }), {
|
||||
|
||||
@@ -146,10 +146,10 @@ const ModelViewer: React.FC<ModelViewerProps> = ({
|
||||
return
|
||||
}
|
||||
|
||||
// Check if modelPath is provided
|
||||
if (!modelPath) {
|
||||
if (!modelPath || modelPath.trim() === '') {
|
||||
console.warn('[ModelViewer] No model path provided')
|
||||
onError?.('Путь к 3D модели не задан')
|
||||
// Не вызываем onError для пустого пути - это нормальное состояние при инициализации
|
||||
setIsLoading(false)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -158,13 +158,24 @@ const ModelViewer: React.FC<ModelViewerProps> = ({
|
||||
return
|
||||
}
|
||||
|
||||
const oldMeshes = sceneRef.current.meshes.slice();
|
||||
oldMeshes.forEach(m => m.dispose());
|
||||
const currentModelPath = modelPath;
|
||||
console.log('[ModelViewer] Starting model load:', currentModelPath);
|
||||
|
||||
setIsLoading(true)
|
||||
setLoadingProgress(0)
|
||||
setShowModel(false)
|
||||
console.log('Loading GLTF model:', modelPath)
|
||||
setModelReady(false)
|
||||
|
||||
const oldMeshes = sceneRef.current.meshes.slice();
|
||||
const activeCameraId = sceneRef.current.activeCamera?.uniqueId;
|
||||
console.log('[ModelViewer] Cleaning up old meshes. Total:', oldMeshes.length);
|
||||
oldMeshes.forEach(m => {
|
||||
if (m.uniqueId !== activeCameraId) {
|
||||
m.dispose();
|
||||
}
|
||||
});
|
||||
|
||||
console.log('[ModelViewer] Loading GLTF model:', currentModelPath)
|
||||
|
||||
// UI элемент загрузчика (есть эффект замедленности)
|
||||
const progressInterval = setInterval(() => {
|
||||
@@ -178,15 +189,44 @@ const ModelViewer: React.FC<ModelViewerProps> = ({
|
||||
}, 100)
|
||||
|
||||
try {
|
||||
const result = await ImportMeshAsync(modelPath, sceneRef.current)
|
||||
console.log('[ModelViewer] Calling ImportMeshAsync with path:', currentModelPath);
|
||||
|
||||
// Проверим доступность файла через fetch
|
||||
try {
|
||||
const testResponse = await fetch(currentModelPath, { method: 'HEAD' });
|
||||
console.log('[ModelViewer] File availability check:', {
|
||||
url: currentModelPath,
|
||||
status: testResponse.status,
|
||||
statusText: testResponse.statusText,
|
||||
ok: testResponse.ok
|
||||
});
|
||||
} catch (fetchError) {
|
||||
console.error('[ModelViewer] File fetch error:', fetchError);
|
||||
}
|
||||
|
||||
const result = await ImportMeshAsync(currentModelPath, sceneRef.current)
|
||||
console.log('[ModelViewer] ImportMeshAsync completed successfully');
|
||||
console.log('[ModelViewer] Import result:', {
|
||||
meshesCount: result.meshes.length,
|
||||
particleSystemsCount: result.particleSystems.length,
|
||||
skeletonsCount: result.skeletons.length,
|
||||
animationGroupsCount: result.animationGroups.length
|
||||
});
|
||||
|
||||
if (isDisposedRef.current || modelPath !== currentModelPath) {
|
||||
console.log('[ModelViewer] Model loading aborted - model changed during load')
|
||||
clearInterval(progressInterval)
|
||||
setIsLoading(false)
|
||||
return;
|
||||
}
|
||||
|
||||
importedMeshesRef.current = result.meshes
|
||||
|
||||
clearInterval(progressInterval)
|
||||
setLoadingProgress(100)
|
||||
|
||||
console.log('GLTF Model loaded successfully!')
|
||||
console.log('ImportMeshAsync result:', result)
|
||||
console.log('[ModelViewer] GLTF Model loaded successfully!', result)
|
||||
|
||||
if (result.meshes.length > 0) {
|
||||
|
||||
const boundingBox = result.meshes[0].getHierarchyBoundingVectors()
|
||||
@@ -210,9 +250,11 @@ const ModelViewer: React.FC<ModelViewerProps> = ({
|
||||
|
||||
// Плавное появление модели
|
||||
setTimeout(() => {
|
||||
if (!isDisposedRef.current) {
|
||||
if (!isDisposedRef.current && modelPath === currentModelPath) {
|
||||
setShowModel(true)
|
||||
setIsLoading(false)
|
||||
} else {
|
||||
console.log('Model display aborted - model changed during animation')
|
||||
}
|
||||
}, 500)
|
||||
} else {
|
||||
@@ -222,9 +264,14 @@ const ModelViewer: React.FC<ModelViewerProps> = ({
|
||||
}
|
||||
} catch (error) {
|
||||
clearInterval(progressInterval)
|
||||
// Only report error if this loading is still relevant
|
||||
if (!isDisposedRef.current && modelPath === currentModelPath) {
|
||||
console.error('Error loading GLTF model:', error)
|
||||
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||
onError?.(`Ошибка загрузки модели: ${errorMessage}`)
|
||||
} else {
|
||||
console.log('Error occurred but loading was aborted - model changed')
|
||||
}
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
@@ -271,9 +318,26 @@ const ModelViewer: React.FC<ModelViewerProps> = ({
|
||||
try {
|
||||
const meta: any = (m as any)?.metadata
|
||||
const extras: any = meta?.gltf?.extras ?? meta?.extras ?? (m as any)?.extras
|
||||
|
||||
const sid = extras?.Sensor_ID ?? extras?.sensor_id ?? extras?.SERIAL_NUMBER ?? extras?.serial_number
|
||||
if (sid == null) return false
|
||||
if (sid != null) {
|
||||
return String(sid).trim() === sensorId
|
||||
}
|
||||
|
||||
const monitoringSensorInstance = extras?.bonsaiPset_ARBM_PSet_MonitoringSensor_Instance
|
||||
if (monitoringSensorInstance && typeof monitoringSensorInstance === 'string') {
|
||||
try {
|
||||
const parsedInstance = JSON.parse(monitoringSensorInstance)
|
||||
const instanceSensorId = parsedInstance?.Sensor_ID
|
||||
if (instanceSensorId != null) {
|
||||
return String(instanceSensorId).trim() === sensorId
|
||||
}
|
||||
} catch (parseError) {
|
||||
console.warn('[ModelViewer] Error parsing MonitoringSensor_Instance JSON:', parseError)
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
} catch (error) {
|
||||
console.warn('[ModelViewer] Error matching sensor mesh:', error)
|
||||
return false
|
||||
|
||||
@@ -166,7 +166,7 @@ const DetectorMenu: React.FC<DetectorMenuProps> = ({ detector, isOpen, onClose,
|
||||
</button>
|
||||
<button className="bg-[rgb(27,29,41)] hover:bg-[rgb(37,39,51)] text-white px-2 py-1 rounded-[8px] text-xs font-medium transition-colors flex items-center gap-1">
|
||||
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4л3 3м6-3а9 9 0 11-18 0 9 9 0 0118 0з" />
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
История
|
||||
</button>
|
||||
@@ -187,13 +187,13 @@ const DetectorMenu: React.FC<DetectorMenuProps> = ({ detector, isOpen, onClose,
|
||||
<div className="flex items-center gap-2">
|
||||
<button className="bg-[rgb(27,29,41)] hover:bg-[rgb(37,39,51)] text-white px-3 py-2 rounded-[10px] text-sm font-medium transition-colors flex items-center gap-2">
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12h6m-6 4h6м2 5H7а2 2 0 01-2-2V5а2 2 0 012-2h5.586а1 1 0 01.707.293л5.414 5.414а1 1 0 01.293.707V19а2 2 0 01-2 2з" />
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
||||
</svg>
|
||||
Отчет
|
||||
</button>
|
||||
<button className="bg-[rgb(27,29,41)] hover:bg-[rgb(37,39,51)] text-white px-3 py-2 rounded-[10px] text-sm font-medium transition-colors flex items-center gap-2">
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4л3 3м6-3а9 9 0 11-18 0 9 9 0 0118 0з" />
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
История
|
||||
</button>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import Image from 'next/image';
|
||||
|
||||
interface MonitoringProps {
|
||||
@@ -12,6 +12,13 @@ const Monitoring: React.FC<MonitoringProps> = ({ onClose, onSelectModel }) => {
|
||||
const [models, setModels] = useState<{ title: string; path: string }[]>([]);
|
||||
const [loadError, setLoadError] = useState<string | null>(null);
|
||||
|
||||
const handleSelectModel = useCallback((modelPath: string) => {
|
||||
console.log(`[NavigationPage] Model selected: ${modelPath}`);
|
||||
onSelectModel?.(modelPath);
|
||||
}, [onSelectModel]);
|
||||
|
||||
console.log('[Monitoring] Models:', models, 'Error:', loadError);
|
||||
|
||||
// Загружаем список доступных моделей из assets/big-models через API
|
||||
useEffect(() => {
|
||||
const fetchModels = async () => {
|
||||
@@ -64,48 +71,56 @@ const Monitoring: React.FC<MonitoringProps> = ({ onClose, onSelectModel }) => {
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="bg-[rgb(158,168,183)] rounded-lg p-3 h-[200px] flex items-center justify-center">
|
||||
<div className="w-full h-full bg-gray-300 rounded flex items-center justify-center">
|
||||
{objectImageError ? (
|
||||
<div className="text-center p-4">
|
||||
<div className="text-gray-600 text-sm font-semibold mb-2">
|
||||
Предпросмотр 3D недоступен
|
||||
</div>
|
||||
<div className="text-gray-500 text-xs">
|
||||
Изображение модели не найдено
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<Image
|
||||
src="/images/test_image.png"
|
||||
alt="Object Model"
|
||||
width={200}
|
||||
height={200}
|
||||
className="max-w-full max-h-full object-contain"
|
||||
style={{ height: 'auto' }}
|
||||
onError={() => setObjectImageError(true)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{loadError && (
|
||||
<div className="rounded-lg bg-red-600/20 border border-red-600/40 text-red-200 text-xs px-3 py-2">
|
||||
Ошибка загрузки списка моделей: {loadError}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{models.length > 0 && (
|
||||
<>
|
||||
{/* Большая панорамная карточка для приоритетной модели */}
|
||||
{models[0] && (
|
||||
<button
|
||||
key={`${models[0].path}-panorama`}
|
||||
type="button"
|
||||
onClick={() => handleSelectModel(models[0].path)}
|
||||
className="w-full bg-gray-300 rounded-lg h-[200px] flex items-center justify-center hover:bg-gray-400 transition-colors mb-4"
|
||||
title={`Загрузить модель: ${models[0].title}`}
|
||||
>
|
||||
<div className="w-full h-full bg-gray-200 rounded flex flex-col items-center justify-center relative">
|
||||
<Image
|
||||
src="/images/test_image.png"
|
||||
alt={models[0].title}
|
||||
width={200}
|
||||
height={200}
|
||||
className="max-w-full max-h-full object-contain opacity-50"
|
||||
style={{ height: 'auto' }}
|
||||
onError={(e) => {
|
||||
const target = e.target as HTMLImageElement;
|
||||
target.style.display = 'none';
|
||||
}}
|
||||
/>
|
||||
<div className="absolute bottom-2 left-2 right-2 text-sm text-gray-700 bg-white/80 rounded px-3 py-1 truncate">
|
||||
{models[0].title}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* Сетка маленьких карточек для остальных моделей */}
|
||||
{models.length > 1 && (
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
{models.length > 0 ? (
|
||||
models.map((model, idx) => (
|
||||
{models.slice(1).map((model, idx) => (
|
||||
<button
|
||||
key={`${model.path}-${idx}`}
|
||||
type="button"
|
||||
onClick={() => onSelectModel?.(model.path)}
|
||||
onClick={() => handleSelectModel(model.path)}
|
||||
className="relative flex-1 bg-gray-300 rounded-lg h-[120px] flex items-center justify-center hover:bg-gray-400 transition-colors"
|
||||
title={`Загрузить модель: ${model.title}`}
|
||||
>
|
||||
<div className="w-full h-full bg-gray-200 rounded flex items-center justify-center">
|
||||
<div className="w-full h-full bg-gray-200 rounded flex flex-col items-center justify-center relative">
|
||||
<Image
|
||||
src="/images/test_image.png"
|
||||
alt={model.title}
|
||||
@@ -118,13 +133,19 @@ const Monitoring: React.FC<MonitoringProps> = ({ onClose, onSelectModel }) => {
|
||||
target.style.display = 'none';
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="absolute bottom-1 left-1 right-1 text-[10px] text-gray-700 bg-white/70 rounded px-2 py-0.5 truncate">
|
||||
{model.title}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</button>
|
||||
))
|
||||
) : (
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{models.length === 0 && (
|
||||
<div className="col-span-2">
|
||||
<div className="rounded-lg bg-gray-200 text-gray-700 text-xs px-3 py-2 border border-gray-300">
|
||||
Список моделей пуст. Добавьте файлы в assets/big-models или проверьте API /api/big-models/list.
|
||||
@@ -133,7 +154,6 @@ const Monitoring: React.FC<MonitoringProps> = ({ onClose, onSelectModel }) => {
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -71,7 +71,7 @@ const Notifications: React.FC<NotificationsProps> = ({ objectId, detectorsData,
|
||||
|
||||
// сортировка по objectId
|
||||
const filteredNotifications = objectId
|
||||
? allNotifications.filter(notification => notification.object.toString() === objectId.toString())
|
||||
? allNotifications.filter(notification => notification.object && notification.object.toString() === objectId.toString())
|
||||
: allNotifications
|
||||
|
||||
// сортировка по timestamp
|
||||
|
||||
Reference in New Issue
Block a user