diff --git a/src/modules/mileage/MonitoringView.tsx b/src/modules/mileage/MonitoringView.tsx index 2fcc305..1b08de3 100644 --- a/src/modules/mileage/MonitoringView.tsx +++ b/src/modules/mileage/MonitoringView.tsx @@ -1,3 +1,774 @@ +import { useState, useEffect, useMemo } from 'react'; +import { motion, AnimatePresence } from 'motion/react'; +import { + Truck, Search, Filter, ChevronDown, + Maximize2, Minimize2, RotateCcw, + ArrowUp, ArrowDown, +} from 'lucide-react'; +import type { MonitoringVehicle } from './types'; +import { fetchMonitoring } from './api'; + +const SearchableSelect = ({ + options, + value, + onChange, + placeholder +}: { + options: string[], + value: string, + onChange: (val: string) => void, + placeholder: string +}) => { + const [isOpen, setIsOpen] = useState(false); + const [search, setSearch] = useState(''); + + const filtered = useMemo(() => { + if (!search) return options; + return options.filter(opt => opt.toLowerCase().includes(search.toLowerCase())); + }, [options, search]); + + return ( +
+
+ setIsOpen(true)} + onChange={(e) => setSearch(e.target.value)} + onBlur={() => { + // Delay to allow clicking an option + setTimeout(() => setIsOpen(false), 200); + }} + /> + +
+ + + {isOpen && ( + +
{ + onChange('All'); + setSearch(''); + setIsOpen(false); + }} + > + 无限制 +
+ {filtered.map(opt => ( +
{ + onChange(opt); + setSearch(''); + setIsOpen(false); + }} + > + {opt} +
+ ))} + {filtered.length === 0 && ( +
+ 无匹配项 +
+ )} +
+ )} +
+
+ ); +}; + export default function MonitoringView() { - return
MonitoringView placeholder
; + const [searchTerm, setSearchTerm] = useState(''); + const [filterDept, setFilterDept] = useState('All'); + const [sortBy, setSortBy] = useState<'today' | 'total'>('today'); + const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('desc'); + const [isFilterOpen, setIsFilterOpen] = useState(false); + const [isFullscreen, setIsFullscreen] = useState(false); + + // New filters from image + const [filterPlate, setFilterPlate] = useState('All'); + const [filterYear, setFilterYear] = useState('All'); + const [filterDateRange, setFilterDateRange] = useState({ start: '', end: '' }); + const [filterDate, setFilterDate] = useState('2026-04-01'); + const [filterProject, setFilterProject] = useState('All'); + const [filterEntity, setFilterEntity] = useState('All'); + const [filterRegionCode, setFilterRegionCode] = useState('All'); + const [filterMileageRange, setFilterMileageRange] = useState({ min: '', max: '' }); + + const [allVehicles, setAllVehicles] = useState([]); + + useEffect(() => { + const load = () => fetchMonitoring().then(d => setAllVehicles(d.vehicles)).catch(() => {}); + load(); + const interval = setInterval(load, 60000); + return () => clearInterval(interval); + }, []); + + const toggleFullscreen = () => { + if (!isFullscreen) { + const elem = document.documentElement; + if (elem.requestFullscreen) { + elem.requestFullscreen().catch(() => {}); + } + } else { + if (document.exitFullscreen) { + document.exitFullscreen().catch(() => {}); + } + } + setIsFullscreen(!isFullscreen); + }; + + const filteredVehicles = useMemo(() => { + return allVehicles.filter(v => { + const matchesSearch = v.plate.toLowerCase().includes(searchTerm.toLowerCase()) || + (v.customer || '').toLowerCase().includes(searchTerm.toLowerCase()); + const matchesDept = filterDept === 'All' || v.department === filterDept; + const matchesPlate = filterPlate === 'All' || v.plate === filterPlate; + const matchesProject = filterProject === 'All' || v.customer === filterProject; + const matchesEntity = filterEntity === 'All' || true; + const matchesRegion = filterRegionCode === 'All' || true; + + // Mileage range filter + const mileage = v.dailyKm || 0; + const minMileage = filterMileageRange.min === '' ? -Infinity : Number(filterMileageRange.min); + const maxMileage = filterMileageRange.max === '' ? Infinity : Number(filterMileageRange.max); + const matchesMileage = mileage >= minMileage && mileage <= maxMileage; + + return matchesSearch && matchesDept && matchesPlate && matchesProject && matchesEntity && matchesRegion && matchesMileage; + }).sort((a, b) => { + const valA = sortBy === 'today' ? (a.dailyKm || 0) : (a.totalKm || 0); + const valB = sortBy === 'today' ? (b.dailyKm || 0) : (b.totalKm || 0); + return sortOrder === 'desc' ? valB - valA : valA - valB; + }); + }, [allVehicles, searchTerm, filterDept, sortBy, sortOrder, filterPlate, filterProject, filterEntity, filterRegionCode, filterMileageRange]); + + const departments = Array.from(new Set(allVehicles.map(v => v.department).filter(Boolean))) as string[]; + const plateNumbers = Array.from(new Set(allVehicles.map(v => v.plate).filter(Boolean))); + const projects = Array.from(new Set(allVehicles.map(v => v.customer).filter(Boolean))) as string[]; + + const stats = useMemo(() => { + const totalToday = filteredVehicles.reduce((sum, v) => sum + (v.dailyKm || 0), 0); + const totalAll = filteredVehicles.reduce((sum, v) => sum + (v.totalKm || 0), 0); + + const activeTotal = sortBy === 'today' ? totalToday : totalAll; + const activeAvg = filteredVehicles.length > 0 ? activeTotal / filteredVehicles.length : 0; + + return { totalToday, totalAll, activeTotal, activeAvg, count: filteredVehicles.length }; + }, [filteredVehicles, sortBy]); + + return ( + <> + {/* Fullscreen Landscape View Overlay */} + + {isFullscreen && ( + + {/* Sidebar / Top Stats in Landscape */} +
+
+
+
+

全屏监控

+
+
+ + +
+
+ + {/* KPI Cards in Fullscreen */} +
+
+
今日总里程
+
+ {stats.totalToday.toLocaleString()} + KM +
+
+
+
累计总里程
+
+ {stats.totalAll.toLocaleString()} + KM +
+
+
+
监控台数
+
+ {stats.count} + +
+
+
+
+ 平均单车 ({sortBy === 'today' ? '今日' : '累计'}) +
+
+ {stats.activeAvg.toFixed(0)} + KM +
+
+
+
+ + {/* Main Table Area */} +
+
+ 车辆实时明细数据 +
+
+
+ 在线 +
+
+
+ 离线 +
+
+
+ 未对接车机 +
+ 最后更新: {new Date().toLocaleTimeString()} +
+
+ +
+ + + + + + + + + + + + + + {filteredVehicles.map((v) => ( + + + + + + + + + + ))} + +
+
+ 车牌号 + +
+
在线状态 +
+ 客户项目 + +
+
+
+ 业务部门 + +
+
{ + if (sortBy === 'today') { + setSortOrder(sortOrder === 'desc' ? 'asc' : 'desc'); + } else { + setSortBy('today'); + setSortOrder('desc'); + } + }} + > +
+ 今日里程 + {sortBy === 'today' && ( + sortOrder === 'desc' ? : + )} +
+
{ + if (sortBy === 'total') { + setSortOrder(sortOrder === 'desc' ? 'asc' : 'desc'); + } else { + setSortBy('total'); + setSortOrder('desc'); + } + }} + > +
+ 累计里程 + {sortBy === 'total' && ( + sortOrder === 'desc' ? : + )} +
+
状态
{v.plate} +
+
+ + {v.isOnline ? '在线' : '离线'} + +
+
{v.customer}{v.department} +
+
+ {!v.isDataSynced && ( +
+ )} + + {v.dailyKm?.toLocaleString()} + +
+ {!v.isDataSynced && 未对接} +
+
+
+ + {v.totalKm?.toLocaleString()} + +
+
+ + {v.isOnline ? '运行中' : '静止/离线'} + +
+
+
+
+ )} +
+ + {/* Ultra Compact Header - Two Rows */} +
+ {/* Top Row: Title & Sort */} +
+
+
+
+
+

里程看板

+ +
+
+ + 实时监控 • 40min更新 +
+
+
+ +
+
+ + +
+
+ +
+
+ + {/* Bottom Row: Quick Filters & Advanced Filter Icon */} +
+
+ + + +
+ + +
+
+ + {/* Expandable Filter Panel */} + + {isFilterOpen && ( + +
+ {/* Search Key */} +
+ +
+ + setSearchTerm(e.target.value)} + /> +
+
+ +
+ {/* Plate Number */} +
+ + +
+ + {/* Year */} +
+ + +
+
+ + {/* Date Range */} +
+ +
+ setFilterDateRange(prev => ({ ...prev, start: e.target.value }))} + /> + - + setFilterDateRange(prev => ({ ...prev, end: e.target.value }))} + /> +
+
+ + {/* Single Date */} +
+ +
+ setFilterDate(e.target.value)} + /> + +
+
+ + {/* Project */} +
+ + +
+ +
+ {/* Department */} +
+ + +
+ + {/* Entity */} +
+ + +
+
+ + {/* Region Code */} +
+ + +
+ + {/* Mileage Range */} +
+ +
+ setFilterMileageRange(prev => ({ ...prev, min: e.target.value }))} + /> + {'\u2264'} 值 {'\u2264'} + setFilterMileageRange(prev => ({ ...prev, max: e.target.value }))} + /> +
+
+ +
+ + +
+
+
+ )} +
+ + {/* High Density KPI Grid */} +
+
+
+ {sortBy === 'today' ? '今日' : '累计'}总里程 (KM) +
+
+ {stats.activeTotal.toLocaleString()} + {sortBy === 'today' && {'\u2191'}12%} +
+
+
+
+
平均单车
+
{stats.activeAvg.toFixed(0)}
+
KM/台
+
+
+
监控台数
+
{stats.count}
+
+
+
+ + {/* High-Density Vehicle List - Two Columns on Tablet, One on Mobile */} +
+
+ 车辆详情清单 + {filteredVehicles.length} 条 +
+ +
+ {filteredVehicles.slice(0, 60).map((v) => ( + { + navigator.clipboard.writeText(v.plate); + }} + > +
+
+
+ +
+
+
+
+
+ {v.plate} + + {v.isOnline ? '在线' : '离线'} + +
+
+ {v.customer || '未分配'} + {v.department?.replace('业务', '')} +
+
+
+
+
+ {!v.isDataSynced && ( +
+ )} +
+ {v.dailyKm?.toLocaleString()} +
+
+
+ + {v.totalKm?.toLocaleString()} + + {!v.isDataSynced && ( + 未同步 + )} +
+
+
+ ))} +
+ + {filteredVehicles.length === 0 && ( +
+

未找到匹配数据

+
+ )} +
+ + ); }