переделана логика загрузки модели, замена страницы Объекты на другой внешний вид, добавление в меню пункта Объекты

This commit is contained in:
2026-02-03 19:00:02 +03:00
parent 458222817e
commit 5e58f6ef76
24 changed files with 3514 additions and 1161 deletions

View File

@@ -3,7 +3,6 @@
import React, { useState, useEffect } from 'react'
import ObjectGallery from '../../../components/objects/ObjectGallery'
import { ObjectData } from '../../../components/objects/ObjectCard'
import Sidebar from '../../../components/ui/Sidebar'
import AnimatedBackground from '../../../components/ui/AnimatedBackground'
import { useRouter } from 'next/navigation'
import Image from 'next/image'
@@ -13,7 +12,6 @@ const transformRawToObjectData = (raw: any): ObjectData => {
const rawId = raw?.id ?? raw?.object_id ?? raw?.uuid ?? raw?.name
const object_id = typeof rawId === 'number' ? `object_${rawId}` : String(rawId ?? '')
// Если объект имеет числовой идентификатор, возвращаем его в виде строки с префиксом 'object_'
const deriveTitle = (): string => {
const t = (raw?.title || '').toString().trim()
if (t) return t
@@ -24,7 +22,6 @@ const transformRawToObjectData = (raw: any): ObjectData => {
if (typeof numMatch === 'number' && !Number.isNaN(numMatch)) {
return `Объект ${numMatch}`
}
// Если объект не имеет числовой идентификатор, возвращаем его строковый идентификатор
return idStr ? `Объект ${idStr}` : `Объект ${object_id}`
}
@@ -79,7 +76,6 @@ const ObjectsPage: React.FC = () => {
} else if (Array.isArray(data?.objects)) {
rawObjectsArray = data.objects
} else if (data && typeof data === 'object') {
// если приходит как map { id: obj }
rawObjectsArray = Object.values(data)
}
@@ -103,8 +99,9 @@ const ObjectsPage: React.FC = () => {
if (loading) {
return (
<div className="flex items-center justify-center min-h-screen bg-[#0e111a]">
<div className="text-center">
<div className="relative flex items-center justify-center min-h-screen overflow-hidden">
<AnimatedBackground />
<div className="relative z-10 text-center">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-500 mx-auto mb-4"></div>
<p className="text-white">Загрузка объектов...</p>
</div>
@@ -114,8 +111,9 @@ const ObjectsPage: React.FC = () => {
if (error) {
return (
<div className="flex items-center justify-center min-h-screen bg-[#0e111a]">
<div className="text-center">
<div className="relative flex items-center justify-center min-h-screen overflow-hidden">
<AnimatedBackground />
<div className="relative z-10 text-center">
<div className="text-red-500 mb-4">
<svg className="w-12 h-12 mx-auto" fill="currentColor" viewBox="0 0 24 24">
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z" />
@@ -130,79 +128,47 @@ const ObjectsPage: React.FC = () => {
}
return (
<div className="relative flex h-screen bg-[#0e111a] overflow-hidden">
<div className="relative flex flex-col h-screen overflow-hidden">
<AnimatedBackground />
<div className="relative z-20">
<Sidebar activeItem={null} />
</div>
<div className="relative z-10 flex-1 overflow-y-auto">
{/* Приветствие и информация */}
<div className="min-h-screen flex flex-col items-center justify-start pt-20 px-8">
{/* Логотип */}
<div className="mb-8 flex justify-center">
<div className="relative w-64 h-20">
<Image
src="/icons/logo.png"
alt="AerBIM Logo"
width={438}
height={60}
className="object-contain"
/>
</div>
</div>
{/* Приветствие */}
<h1 className="text-5xl font-bold text-white mb-4 text-center animate-fade-in" style={{ fontFamily: 'Inter, sans-serif' }}>
Добро пожаловать!
</h1>
<p className="text-xl text-gray-300 mb-8 text-center animate-fade-in" style={{ animationDelay: '0.2s', fontFamily: 'Inter, sans-serif' }}>
Система мониторинга AerBIM Monitor
</p>
{/* Версия системы */}
<div className="mb-16 p-4 rounded-lg bg-gradient-to-r from-blue-500/10 to-cyan-500/10 border border-blue-500/20 inline-block animate-fade-in" style={{ animationDelay: '0.4s' }}>
<p className="text-sm text-gray-400" style={{ fontFamily: 'Inter, sans-serif' }}>
Версия системы: <span className="text-cyan-400 font-semibold">3.0.0</span>
</p>
</div>
{/* Блок с галереей объектов */}
<div className="w-full max-w-6xl p-8 rounded-xl bg-gradient-to-r from-blue-500/10 to-cyan-500/10 border border-blue-500/20 backdrop-blur-sm">
{/* Заголовок галереи */}
<h2 className="text-3xl font-bold text-white mb-8 text-center" style={{ fontFamily: 'Inter, sans-serif' }}>
Выберите объект для работы
</h2>
{/* Галерея объектов */}
<ObjectGallery
objects={objects}
title=""
onObjectSelect={handleObjectSelect}
selectedObjectId={selectedObjectId}
{/* Header */}
<header className="relative z-20 bg-[#161824]/80 backdrop-blur-sm border-b border-blue-500/20">
<div className="flex items-center justify-between px-6 py-3">
<div className="flex items-center gap-4">
<Image
src="/icons/logo.png"
alt="AerBIM Logo"
width={150}
height={40}
className="object-contain"
/>
</div>
<div className="flex items-center gap-4">
<span className="text-sm text-gray-400">
Версия: <span className="text-cyan-400 font-semibold">3.0.0</span>
</span>
</div>
</div>
</header>
{/* Main Content */}
<div className="relative z-10 flex-1 overflow-y-auto">
<div className="max-w-7xl mx-auto px-6 py-6">
{/* Заголовок */}
<h1 className="text-2xl font-bold text-white mb-6">
Выберите объект для работы
</h1>
{/* Галерея объектов */}
<ObjectGallery
objects={objects}
title=""
onObjectSelect={handleObjectSelect}
selectedObjectId={selectedObjectId}
/>
</div>
</div>
<style jsx>{`
@keyframes fade-in {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.animate-fade-in {
animation: fade-in 0.8s ease-out forwards;
}
`}</style>
</div>
)
}

View File

@@ -0,0 +1,208 @@
'use client'
import React, { useState, useEffect } from 'react'
import ObjectGallery from '../../../components/objects/ObjectGallery'
import { ObjectData } from '../../../components/objects/ObjectCard'
import Sidebar from '../../../components/ui/Sidebar'
import AnimatedBackground from '../../../components/ui/AnimatedBackground'
import { useRouter } from 'next/navigation'
import Image from 'next/image'
// Универсальная функция для преобразования объекта из бэкенда в ObjectData
const transformRawToObjectData = (raw: any): ObjectData => {
const rawId = raw?.id ?? raw?.object_id ?? raw?.uuid ?? raw?.name
const object_id = typeof rawId === 'number' ? `object_${rawId}` : String(rawId ?? '')
// Если объект имеет числовой идентификатор, возвращаем его в виде строки с префиксом 'object_'
const deriveTitle = (): string => {
const t = (raw?.title || '').toString().trim()
if (t) return t
const idStr = String(rawId ?? '').toString()
const numMatch = typeof rawId === 'number'
? rawId
: (() => { const m = idStr.match(/\d+/); return m ? Number(m[0]) : undefined })()
if (typeof numMatch === 'number' && !Number.isNaN(numMatch)) {
return `Объект ${numMatch}`
}
// Если объект не имеет числовой идентификатор, возвращаем его строковый идентификатор
return idStr ? `Объект ${idStr}` : `Объект ${object_id}`
}
return {
object_id,
title: deriveTitle(),
description: raw?.description ?? `Описание объекта ${raw?.title ?? object_id}`,
image: raw?.image ?? null,
location: raw?.location ?? raw?.address ?? 'Не указано',
floors: Number(raw?.floors ?? 0),
area: String(raw?.area ?? ''),
type: raw?.type ?? 'object',
status: raw?.status ?? 'active',
}
}
const ObjectsPage: React.FC = () => {
const [objects, setObjects] = useState<ObjectData[]>([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
const [selectedObjectId, setSelectedObjectId] = useState<string | null>(null)
const router = useRouter()
useEffect(() => {
const loadData = async () => {
setLoading(true)
setError(null)
try {
const url = '/api/get-objects'
const res = await fetch(url, { cache: 'no-store' })
const payloadText = await res.text()
let payload: any
try { payload = JSON.parse(payloadText) } catch { payload = payloadText }
console.log('[ObjectsPage] GET /api/get-objects', { status: res.status, payload })
if (!res.ok) {
const errorMessage = typeof payload === 'string' ? payload : (payload?.error || 'Не удалось получить данные объектов')
if (errorMessage.includes('Authentication required') || res.status === 401) {
console.log('[ObjectsPage] Authentication required, redirecting to login')
router.push('/login')
return
}
throw new Error(errorMessage)
}
const data = (payload?.data ?? payload) as any
let rawObjectsArray: any[] = []
if (Array.isArray(data)) {
rawObjectsArray = data
} else if (Array.isArray(data?.objects)) {
rawObjectsArray = data.objects
} else if (data && typeof data === 'object') {
// если приходит как map { id: obj }
rawObjectsArray = Object.values(data)
}
const transformedObjects = rawObjectsArray.map(transformRawToObjectData)
setObjects(transformedObjects)
} catch (err: any) {
console.error('Ошибка при загрузке данных объектов:', err)
setError(err?.message || 'Произошла неизвестная ошибка')
} finally {
setLoading(false)
}
}
loadData()
}, [router])
const handleObjectSelect = (objectId: string) => {
console.log('Object selected:', objectId)
setSelectedObjectId(objectId)
}
if (loading) {
return (
<div className="flex items-center justify-center min-h-screen bg-[#0e111a]">
<div className="text-center">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-500 mx-auto mb-4"></div>
<p className="text-white">Загрузка объектов...</p>
</div>
</div>
)
}
if (error) {
return (
<div className="flex items-center justify-center min-h-screen bg-[#0e111a]">
<div className="text-center">
<div className="text-red-500 mb-4">
<svg className="w-12 h-12 mx-auto" fill="currentColor" viewBox="0 0 24 24">
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z" />
</svg>
</div>
<h3 className="text-lg font-medium text-white mb-2">Ошибка загрузки данных</h3>
<p className="text-[#71717a] mb-4">{error}</p>
<p className="text-sm text-gray-500">Если проблема повторяется, обратитесь к администратору</p>
</div>
</div>
)
}
return (
<div className="relative flex h-screen bg-[#0e111a] overflow-hidden">
<AnimatedBackground />
{/* Sidebar скрыт на странице выбора объектов */}
<div className="relative z-10 flex-1 overflow-y-auto">
{/* Приветствие и информация */}
<div className="min-h-screen flex flex-col items-center justify-start pt-8 px-8">
{/* Логотип */}
<div className="mb-4 flex justify-center">
<div className="relative w-64 h-20">
<Image
src="/icons/logo.png"
alt="AerBIM Logo"
width={438}
height={60}
className="object-contain"
/>
</div>
</div>
{/* Приветствие */}
<h1 className="text-5xl font-bold text-white mb-4 text-center animate-fade-in" style={{ fontFamily: 'Inter, sans-serif' }}>
Добро пожаловать!
</h1>
<p className="text-xl text-gray-300 mb-8 text-center animate-fade-in" style={{ animationDelay: '0.2s', fontFamily: 'Inter, sans-serif' }}>
Система мониторинга AerBIM Monitor
</p>
{/* Версия системы */}
<div className="mb-6 p-3 rounded-lg bg-gradient-to-r from-blue-500/10 to-cyan-500/10 border border-blue-500/20 inline-block">
<p className="text-sm text-gray-400" style={{ fontFamily: 'Inter, sans-serif' }}>
Версия системы: <span className="text-cyan-400 font-semibold">3.0.0</span>
</p>
</div>
{/* Блок с галереей объектов */}
<div className="w-full max-w-6xl p-4 rounded-xl bg-gradient-to-r from-blue-500/10 to-cyan-500/10 border border-blue-500/20 backdrop-blur-sm">
{/* Заголовок галереи */}
<h2 className="text-2xl font-bold text-white mb-4 text-center" style={{ fontFamily: 'Inter, sans-serif' }}>
Выберите объект для работы
</h2>
{/* Галерея объектов */}
<ObjectGallery
objects={objects}
title=""
onObjectSelect={handleObjectSelect}
selectedObjectId={selectedObjectId}
/>
</div>
</div>
</div>
<style jsx>{`
@keyframes fade-in {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.animate-fade-in {
animation: fade-in 0.8s ease-out forwards;
}
`}</style>
</div>
)
}
export default ObjectsPage