Added more detectors to DB
This commit is contained in:
@@ -95,11 +95,18 @@ const NavigationPage: React.FC = () => {
|
|||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const handleModelError = useCallback((error: string) => {
|
const handleModelError = useCallback((error: string) => {
|
||||||
console.error('Model loading error:', error)
|
console.error('[NavigationPage] Model loading error:', error)
|
||||||
setModelError(error)
|
setModelError(error)
|
||||||
setIsModelReady(false)
|
setIsModelReady(false)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (selectedModelPath) {
|
||||||
|
setIsModelReady(false);
|
||||||
|
setModelError(null);
|
||||||
|
}
|
||||||
|
}, [selectedModelPath]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (urlObjectId && urlObjectTitle && (!currentObject.id || currentObject.id !== urlObjectId)) {
|
if (urlObjectId && urlObjectTitle && (!currentObject.id || currentObject.id !== urlObjectId)) {
|
||||||
setCurrentObject(urlObjectId, urlObjectTitle)
|
setCurrentObject(urlObjectId, urlObjectTitle)
|
||||||
@@ -206,6 +213,7 @@ const NavigationPage: React.FC = () => {
|
|||||||
objectId={objectId || undefined}
|
objectId={objectId || undefined}
|
||||||
onClose={closeMonitoring}
|
onClose={closeMonitoring}
|
||||||
onSelectModel={(path) => {
|
onSelectModel={(path) => {
|
||||||
|
console.log('[NavigationPage] Model selected:', path);
|
||||||
setSelectedModelPath(path)
|
setSelectedModelPath(path)
|
||||||
setModelError(null)
|
setModelError(null)
|
||||||
setIsModelReady(false)
|
setIsModelReady(false)
|
||||||
@@ -291,20 +299,37 @@ const NavigationPage: React.FC = () => {
|
|||||||
<div className="flex-1 overflow-hidden">
|
<div className="flex-1 overflow-hidden">
|
||||||
<div className="h-full">
|
<div className="h-full">
|
||||||
{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">
|
{console.log('[NavigationPage] Rendering error message, modelError:', modelError)}
|
||||||
<div className="text-red-400 text-lg font-semibold mb-4">
|
<div className="h-full flex items-center justify-center bg-[#0e111a]">
|
||||||
Ошибка загрузки 3D модели
|
<div className="text-center p-8 bg-[#161824] rounded-lg border border-gray-700 max-w-md">
|
||||||
</div>
|
<div className="text-red-400 text-lg font-semibold mb-4">
|
||||||
<div className="text-gray-300 mb-4">
|
Ошибка загрузки 3D модели
|
||||||
{modelError}
|
</div>
|
||||||
</div>
|
<div className="text-gray-300 mb-4">
|
||||||
<div className="text-sm text-gray-400">
|
{modelError}
|
||||||
Используйте навигацию по этажам для просмотра детекторов
|
</div>
|
||||||
</div>
|
<div className="text-sm text-gray-400">
|
||||||
|
Используйте навигацию по этажам для просмотра детекторов
|
||||||
</div>
|
</div>
|
||||||
</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
|
<ModelViewer
|
||||||
modelPath={selectedModelPath}
|
modelPath={selectedModelPath}
|
||||||
onModelLoaded={handleModelLoaded}
|
onModelLoaded={handleModelLoaded}
|
||||||
|
|||||||
@@ -28,8 +28,7 @@ export async function GET() {
|
|||||||
title = title.replace(/\bprop\b/gi, '');
|
title = title.replace(/\bprop\b/gi, '');
|
||||||
title = title.replace(/\s{2,}/g, ' ').trim();
|
title = title.replace(/\s{2,}/g, ' ').trim();
|
||||||
|
|
||||||
const pathUrl = `/static-models/${filename}`;
|
return { name: title, path: `/static-models/${filename}` };
|
||||||
return { name: title, path: pathUrl };
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return new Response(JSON.stringify({ models }), {
|
return new Response(JSON.stringify({ models }), {
|
||||||
|
|||||||
@@ -146,10 +146,10 @@ const ModelViewer: React.FC<ModelViewerProps> = ({
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if modelPath is provided
|
if (!modelPath || modelPath.trim() === '') {
|
||||||
if (!modelPath) {
|
|
||||||
console.warn('[ModelViewer] No model path provided')
|
console.warn('[ModelViewer] No model path provided')
|
||||||
onError?.('Путь к 3D модели не задан')
|
// Не вызываем onError для пустого пути - это нормальное состояние при инициализации
|
||||||
|
setIsLoading(false)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -158,13 +158,24 @@ const ModelViewer: React.FC<ModelViewerProps> = ({
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const oldMeshes = sceneRef.current.meshes.slice();
|
const currentModelPath = modelPath;
|
||||||
oldMeshes.forEach(m => m.dispose());
|
console.log('[ModelViewer] Starting model load:', currentModelPath);
|
||||||
|
|
||||||
setIsLoading(true)
|
setIsLoading(true)
|
||||||
setLoadingProgress(0)
|
setLoadingProgress(0)
|
||||||
setShowModel(false)
|
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 элемент загрузчика (есть эффект замедленности)
|
// UI элемент загрузчика (есть эффект замедленности)
|
||||||
const progressInterval = setInterval(() => {
|
const progressInterval = setInterval(() => {
|
||||||
@@ -178,15 +189,44 @@ const ModelViewer: React.FC<ModelViewerProps> = ({
|
|||||||
}, 100)
|
}, 100)
|
||||||
|
|
||||||
try {
|
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
|
importedMeshesRef.current = result.meshes
|
||||||
|
|
||||||
clearInterval(progressInterval)
|
clearInterval(progressInterval)
|
||||||
setLoadingProgress(100)
|
setLoadingProgress(100)
|
||||||
|
|
||||||
console.log('GLTF Model loaded successfully!')
|
console.log('[ModelViewer] GLTF Model loaded successfully!', result)
|
||||||
console.log('ImportMeshAsync result:', result)
|
|
||||||
if (result.meshes.length > 0) {
|
if (result.meshes.length > 0) {
|
||||||
|
|
||||||
const boundingBox = result.meshes[0].getHierarchyBoundingVectors()
|
const boundingBox = result.meshes[0].getHierarchyBoundingVectors()
|
||||||
@@ -210,9 +250,11 @@ const ModelViewer: React.FC<ModelViewerProps> = ({
|
|||||||
|
|
||||||
// Плавное появление модели
|
// Плавное появление модели
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (!isDisposedRef.current) {
|
if (!isDisposedRef.current && modelPath === currentModelPath) {
|
||||||
setShowModel(true)
|
setShowModel(true)
|
||||||
setIsLoading(false)
|
setIsLoading(false)
|
||||||
|
} else {
|
||||||
|
console.log('Model display aborted - model changed during animation')
|
||||||
}
|
}
|
||||||
}, 500)
|
}, 500)
|
||||||
} else {
|
} else {
|
||||||
@@ -222,9 +264,14 @@ const ModelViewer: React.FC<ModelViewerProps> = ({
|
|||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
clearInterval(progressInterval)
|
clearInterval(progressInterval)
|
||||||
console.error('Error loading GLTF model:', error)
|
// Only report error if this loading is still relevant
|
||||||
const errorMessage = error instanceof Error ? error.message : String(error)
|
if (!isDisposedRef.current && modelPath === currentModelPath) {
|
||||||
onError?.(`Ошибка загрузки модели: ${errorMessage}`)
|
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)
|
setIsLoading(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -271,9 +318,26 @@ const ModelViewer: React.FC<ModelViewerProps> = ({
|
|||||||
try {
|
try {
|
||||||
const meta: any = (m as any)?.metadata
|
const meta: any = (m as any)?.metadata
|
||||||
const extras: any = meta?.gltf?.extras ?? meta?.extras ?? (m as any)?.extras
|
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
|
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
|
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) {
|
} catch (error) {
|
||||||
console.warn('[ModelViewer] Error matching sensor mesh:', error)
|
console.warn('[ModelViewer] Error matching sensor mesh:', error)
|
||||||
return false
|
return false
|
||||||
|
|||||||
@@ -166,7 +166,7 @@ const DetectorMenu: React.FC<DetectorMenuProps> = ({ detector, isOpen, onClose,
|
|||||||
</button>
|
</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">
|
<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">
|
<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>
|
</svg>
|
||||||
История
|
История
|
||||||
</button>
|
</button>
|
||||||
@@ -187,13 +187,13 @@ const DetectorMenu: React.FC<DetectorMenuProps> = ({ detector, isOpen, onClose,
|
|||||||
<div className="flex items-center gap-2">
|
<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">
|
<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">
|
<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>
|
</svg>
|
||||||
Отчет
|
Отчет
|
||||||
</button>
|
</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">
|
<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">
|
<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>
|
</svg>
|
||||||
История
|
История
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect, useCallback } from 'react';
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
|
|
||||||
interface MonitoringProps {
|
interface MonitoringProps {
|
||||||
@@ -12,6 +12,13 @@ const Monitoring: React.FC<MonitoringProps> = ({ onClose, onSelectModel }) => {
|
|||||||
const [models, setModels] = useState<{ title: string; path: string }[]>([]);
|
const [models, setModels] = useState<{ title: string; path: string }[]>([]);
|
||||||
const [loadError, setLoadError] = useState<string | null>(null);
|
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
|
// Загружаем список доступных моделей из assets/big-models через API
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchModels = async () => {
|
const fetchModels = async () => {
|
||||||
@@ -64,53 +71,29 @@ const Monitoring: React.FC<MonitoringProps> = ({ onClose, onSelectModel }) => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</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 && (
|
{loadError && (
|
||||||
<div className="rounded-lg bg-red-600/20 border border-red-600/40 text-red-200 text-xs px-3 py-2">
|
<div className="rounded-lg bg-red-600/20 border border-red-600/40 text-red-200 text-xs px-3 py-2">
|
||||||
Ошибка загрузки списка моделей: {loadError}
|
Ошибка загрузки списка моделей: {loadError}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-3">
|
{models.length > 0 && (
|
||||||
{models.length > 0 ? (
|
<>
|
||||||
models.map((model, idx) => (
|
{/* Большая панорамная карточка для приоритетной модели */}
|
||||||
|
{models[0] && (
|
||||||
<button
|
<button
|
||||||
key={`${model.path}-${idx}`}
|
key={`${models[0].path}-panorama`}
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => onSelectModel?.(model.path)}
|
onClick={() => handleSelectModel(models[0].path)}
|
||||||
className="relative flex-1 bg-gray-300 rounded-lg h-[120px] flex items-center justify-center hover:bg-gray-400 transition-colors"
|
className="w-full bg-gray-300 rounded-lg h-[200px] flex items-center justify-center hover:bg-gray-400 transition-colors mb-4"
|
||||||
title={`Загрузить модель: ${model.title}`}
|
title={`Загрузить модель: ${models[0].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
|
<Image
|
||||||
src="/images/test_image.png"
|
src="/images/test_image.png"
|
||||||
alt={model.title}
|
alt={models[0].title}
|
||||||
width={120}
|
width={200}
|
||||||
height={120}
|
height={200}
|
||||||
className="max-w-full max-h-full object-contain opacity-50"
|
className="max-w-full max-h-full object-contain opacity-50"
|
||||||
style={{ height: 'auto' }}
|
style={{ height: 'auto' }}
|
||||||
onError={(e) => {
|
onError={(e) => {
|
||||||
@@ -118,20 +101,57 @@ const Monitoring: React.FC<MonitoringProps> = ({ onClose, onSelectModel }) => {
|
|||||||
target.style.display = 'none';
|
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>
|
</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>
|
|
||||||
</button>
|
</button>
|
||||||
))
|
)}
|
||||||
) : (
|
|
||||||
<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">
|
{models.length > 1 && (
|
||||||
Список моделей пуст. Добавьте файлы в assets/big-models или проверьте API /api/big-models/list.
|
<div className="grid grid-cols-2 gap-3">
|
||||||
|
{models.slice(1).map((model, idx) => (
|
||||||
|
<button
|
||||||
|
key={`${model.path}-${idx}`}
|
||||||
|
type="button"
|
||||||
|
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 flex-col items-center justify-center relative">
|
||||||
|
<Image
|
||||||
|
src="/images/test_image.png"
|
||||||
|
alt={model.title}
|
||||||
|
width={120}
|
||||||
|
height={120}
|
||||||
|
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-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>
|
</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.
|
||||||
</div>
|
</div>
|
||||||
)}
|
</div>
|
||||||
</div>
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ const Notifications: React.FC<NotificationsProps> = ({ objectId, detectorsData,
|
|||||||
|
|
||||||
// сортировка по objectId
|
// сортировка по objectId
|
||||||
const filteredNotifications = objectId
|
const filteredNotifications = objectId
|
||||||
? allNotifications.filter(notification => notification.object.toString() === objectId.toString())
|
? allNotifications.filter(notification => notification.object && notification.object.toString() === objectId.toString())
|
||||||
: allNotifications
|
: allNotifications
|
||||||
|
|
||||||
// сортировка по timestamp
|
// сортировка по timestamp
|
||||||
|
|||||||
Reference in New Issue
Block a user