Разработка интерфейса фронт
This commit is contained in:
206
frontend/components/navigation/FloorNavigation.tsx
Normal file
206
frontend/components/navigation/FloorNavigation.tsx
Normal file
@@ -0,0 +1,206 @@
|
||||
'use client'
|
||||
|
||||
import React, { useState } from 'react'
|
||||
|
||||
interface DetectorsDataType {
|
||||
detectors: Record<string, DetectorType>
|
||||
}
|
||||
|
||||
interface FloorNavigationProps {
|
||||
objectId?: string
|
||||
detectorsData: DetectorsDataType
|
||||
onDetectorMenuClick: (detector: DetectorType) => void
|
||||
onClose?: () => void
|
||||
}
|
||||
|
||||
interface DetectorType {
|
||||
detector_id: number
|
||||
name: string
|
||||
object: string
|
||||
status: string
|
||||
checked: boolean
|
||||
type: string
|
||||
location: string
|
||||
floor: number
|
||||
notifications: Array<{
|
||||
id: number
|
||||
type: string
|
||||
message: string
|
||||
timestamp: string
|
||||
acknowledged: boolean
|
||||
priority: string
|
||||
}>
|
||||
}
|
||||
|
||||
const FloorNavigation: React.FC<FloorNavigationProps> = ({ objectId, detectorsData, onDetectorMenuClick, onClose }) => {
|
||||
const [expandedFloors, setExpandedFloors] = useState<Set<number>>(new Set())
|
||||
const [searchTerm, setSearchTerm] = useState('')
|
||||
|
||||
// конвертация детекторов в array и фильтруем по objectId и тексту запроса
|
||||
const detectorsArray = Object.values(detectorsData.detectors) as DetectorType[]
|
||||
let filteredDetectors = objectId
|
||||
? detectorsArray.filter(detector => detector.object === objectId)
|
||||
: detectorsArray
|
||||
|
||||
// Фильтр-поиск
|
||||
if (searchTerm) {
|
||||
filteredDetectors = filteredDetectors.filter(detector =>
|
||||
detector.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
detector.location.toLowerCase().includes(searchTerm.toLowerCase())
|
||||
)
|
||||
}
|
||||
|
||||
// Группиовка детекторов по этажам
|
||||
const detectorsByFloor = filteredDetectors.reduce((acc, detector) => {
|
||||
const floor = detector.floor
|
||||
if (!acc[floor]) {
|
||||
acc[floor] = []
|
||||
}
|
||||
acc[floor].push(detector)
|
||||
return acc
|
||||
}, {} as Record<number, DetectorType[]>)
|
||||
|
||||
// Сортировка этажей
|
||||
const sortedFloors = Object.keys(detectorsByFloor)
|
||||
.map(Number)
|
||||
.sort((a, b) => a - b)
|
||||
|
||||
const toggleFloor = (floor: number) => {
|
||||
const newExpanded = new Set(expandedFloors)
|
||||
if (newExpanded.has(floor)) {
|
||||
newExpanded.delete(floor)
|
||||
} else {
|
||||
newExpanded.add(floor)
|
||||
}
|
||||
setExpandedFloors(newExpanded)
|
||||
}
|
||||
|
||||
const getStatusColor = (status: string) => {
|
||||
switch (status) {
|
||||
case '#b3261e': return 'bg-red-500'
|
||||
case '#fd7c22': return 'bg-orange-500'
|
||||
case '#00ff00': return 'bg-green-500'
|
||||
default: return 'bg-gray-500'
|
||||
}
|
||||
}
|
||||
|
||||
const getStatusText = (status: string) => {
|
||||
switch (status) {
|
||||
case '#b3261e': return 'Критический'
|
||||
case '#fd7c22': return 'Предупреждение'
|
||||
case '#00ff00': return 'Норма'
|
||||
default: return 'Неизвестно'
|
||||
}
|
||||
}
|
||||
|
||||
const handleDetectorMenuClick = (detector: DetectorType) => {
|
||||
onDetectorMenuClick(detector)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="w-full max-w-2xl">
|
||||
<div className="bg-[rgb(22,24,36)] rounded-[12px] p-4 space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="text-white text-2xl font-semibold">Навигация по этажам</h2>
|
||||
{onClose && (
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="text-white hover:text-gray-300 transition-colors"
|
||||
>
|
||||
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex-1 relative">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Поиск детекторов..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="w-full bg-[rgb(27,30,40)] text-white placeholder-gray-400 px-4 py-2 rounded-lg border border-gray-600 focus:border-blue-500 focus:outline-none"
|
||||
/>
|
||||
<svg className="absolute right-3 top-2.5 w-5 h-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
||||
</svg>
|
||||
</div>
|
||||
<button className="bg-[rgb(27,29,41)] hover:bg-[rgb(37,39,51)] text-white px-4 py-2 rounded-lg 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 6V4m0 2a2 2 0 100 4m0-4a2 2 0 110 4m-6 8a2 2 0 100-4m0 4a2 2 0 100 4m0-4v2m0-6V4m6 6v10m6-2a2 2 0 100-4m0 4a2 2 0 100 4m0-4v2m0-6V4" />
|
||||
</svg>
|
||||
Фильтр
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
{sortedFloors.map(floor => {
|
||||
const floorDetectors = detectorsByFloor[floor]
|
||||
const isExpanded = expandedFloors.has(floor)
|
||||
|
||||
return (
|
||||
<div key={floor} className="bg-[rgb(27,30,40)] rounded-lg overflow-hidden">
|
||||
<button
|
||||
onClick={() => toggleFloor(floor)}
|
||||
className="w-full px-4 py-3 flex items-center justify-between hover:bg-[rgb(53,58,70)] transition-colors"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-white font-medium">{floor} этаж</span>
|
||||
<span className="text-gray-400 text-sm">({floorDetectors.length} детекторов)</span>
|
||||
</div>
|
||||
<svg
|
||||
className={`w-5 h-5 text-gray-400 transition-transform ${
|
||||
isExpanded ? 'rotate-180' : ''
|
||||
}`}
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
{/* суб-меню с детекторами */}
|
||||
{isExpanded && (
|
||||
<div className="px-4 pb-3 space-y-2">
|
||||
{floorDetectors.map(detector => (
|
||||
<div
|
||||
key={detector.detector_id}
|
||||
className="bg-[rgb(53,58,70)] rounded-md p-3 flex items-center justify-between"
|
||||
>
|
||||
<div className="flex-1">
|
||||
<div className="text-white text-sm font-medium">{detector.name}</div>
|
||||
<div className="text-gray-400 text-xs">{detector.location}</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className={`w-3 h-3 rounded-full ${getStatusColor(detector.status)}`}></div>
|
||||
<span className="text-xs text-gray-300">{getStatusText(detector.status)}</span>
|
||||
{detector.checked && (
|
||||
<svg className="w-4 h-4 text-green-400" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
|
||||
</svg>
|
||||
)}
|
||||
<button
|
||||
onClick={() => handleDetectorMenuClick(detector)}
|
||||
className="w-6 h-6 bg-[rgb(27,29,41)] hover:bg-[rgb(37,39,51)] rounded-full flex items-center justify-center transition-colors relative"
|
||||
>
|
||||
<div className="w-2 h-2 bg-white rounded-full"></div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default FloorNavigation
|
||||
Reference in New Issue
Block a user