import { useState, useEffect, useCallback, useMemo, useRef } from 'react'; import { motion, AnimatePresence } from 'motion/react'; import { Truck, Search, Filter, ChevronDown, Maximize2, Minimize2, RotateCcw, ArrowUp, ArrowDown, ChevronsUp, } from 'lucide-react'; import type { MonitoringVehicle, MonitoringStats, MonitoringFilters } 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: string) => (
{ onChange(opt); setSearch(''); setIsOpen(false); }} > {opt}
))} {filtered.length === 0 && (
无匹配项
)}
)}
); }; export default function MonitoringView() { 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 [filterCustomer, setFilterCustomer] = useState('All'); const [filterProject, setFilterProject] = useState('All'); const [filterEntity, setFilterEntity] = useState('All'); const [filterRegionCode, setFilterRegionCode] = useState('All'); const [filterMileageRange, setFilterMileageRange] = useState({ min: '', max: '' }); const [appliedMileageRange, setAppliedMileageRange] = useState({ min: '', max: '' }); const [filterDate, setFilterDate] = useState(() => new Date().toISOString().split('T')[0]); const [vehicles, setVehicles] = useState([]); const [stats, setStats] = useState({ totalToday: 0, totalAll: 0, vehicleCount: 0, yesterdayTotal: 0 }); const [filterOptions, setFilterOptions] = useState({ departments: [], customers: [], plates: [], projects: [], entities: [] }); const [total, setTotal] = useState(0); const [page, setPage] = useState(1); const [hasMore, setHasMore] = useState(true); const [loadingMore, setLoadingMore] = useState(false); const [showBackToTop, setShowBackToTop] = useState(false); const PAGE_SIZE = 50; const departments = filterOptions.departments; const plateNumbers = filterOptions.plates; // 加载首页数据 const loadFirstPage = useCallback(() => { fetchMonitoring({ sortBy, sortOrder, limit: PAGE_SIZE, page: 1, search: searchTerm || undefined, dept: filterDept !== 'All' ? filterDept : undefined, customer: filterCustomer !== 'All' ? filterCustomer : undefined, project: filterProject !== 'All' ? filterProject : undefined, entity: filterEntity !== 'All' ? filterEntity : undefined, plate: filterPlate !== 'All' ? filterPlate : undefined, mileageMin: appliedMileageRange.min || undefined, mileageMax: appliedMileageRange.max || undefined, date: filterDate || undefined, }).then(d => { setVehicles(d.vehicles); setStats(d.stats); setFilterOptions(d.filters); setTotal(d.total); setPage(1); setHasMore(d.page < d.totalPages); }).catch(() => {}); }, [sortBy, sortOrder, searchTerm, filterDept, filterCustomer, filterProject, filterEntity, filterPlate, appliedMileageRange, filterDate]); // 加载更多 const loadMore = useCallback(() => { if (loadingMore || !hasMore) return; const nextPage = page + 1; setLoadingMore(true); fetchMonitoring({ sortBy, sortOrder, limit: PAGE_SIZE, page: nextPage, search: searchTerm || undefined, dept: filterDept !== 'All' ? filterDept : undefined, customer: filterCustomer !== 'All' ? filterCustomer : undefined, project: filterProject !== 'All' ? filterProject : undefined, entity: filterEntity !== 'All' ? filterEntity : undefined, plate: filterPlate !== 'All' ? filterPlate : undefined, mileageMin: appliedMileageRange.min || undefined, mileageMax: appliedMileageRange.max || undefined, date: filterDate || undefined, }).then(d => { setVehicles(prev => [...prev, ...d.vehicles]); setPage(nextPage); setHasMore(nextPage < d.totalPages); }).catch(() => {}).finally(() => setLoadingMore(false)); }, [sortBy, sortOrder, searchTerm, filterDept, filterCustomer, filterProject, filterEntity, filterPlate, appliedMileageRange, filterDate, page, loadingMore, hasMore]); // 筛选/排序变化时重新加载 useEffect(() => { loadFirstPage(); }, [loadFirstPage]); // 触底检测:用 IntersectionObserver 监听哨兵元素 const loadMoreRef = useRef(loadMore); loadMoreRef.current = loadMore; const sentinelRef = useRef(null); useEffect(() => { const sentinel = sentinelRef.current; if (!sentinel) return; const observer = new IntersectionObserver( (entries) => { if (entries[0].isIntersecting) { loadMoreRef.current(); } }, { rootMargin: '200px' } ); observer.observe(sentinel); return () => observer.disconnect(); }, []); // 回到顶部按钮:用 IntersectionObserver 检测顶部哨兵是否离开视口 const topSentinelRef = useRef(null); useEffect(() => { const el = topSentinelRef.current; if (!el) return; const observer = new IntersectionObserver( ([entry]) => setShowBackToTop(!entry.isIntersecting), ); observer.observe(el); return () => observer.disconnect(); }, []); const scrollToTop = () => { window.scrollTo({ top: 0, behavior: 'smooth' }); document.documentElement.scrollTop = 0; }; const filteredVehicles = vehicles; const toggleFullscreen = () => { setIsFullscreen(!isFullscreen); }; // 全屏时禁止背景滚动 useEffect(() => { if (isFullscreen) { document.body.style.overflow = 'hidden'; } else { document.body.style.overflow = ''; } return () => { document.body.style.overflow = ''; }; }, [isFullscreen]); return ( <> {/* 顶部哨兵:离开视口时显示回到顶部按钮 */}
{/* Fullscreen Landscape View Overlay */} {isFullscreen && ( {/* Top bar: title + KPI row + close */}

全屏监控

{/* KPI — single row */}
今日总里程
{Math.round(stats.totalToday).toLocaleString()} km
累计总里程
{Math.round(stats.totalAll).toLocaleString()} km
监控台数
{stats.vehicleCount}
平均单车
{(stats.vehicleCount > 0 ? (sortBy === 'today' ? stats.totalToday : stats.totalAll) / stats.vehicleCount : 0).toFixed(0)} km
{/* 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.rentStatus || ''}
{!v.isDataSynced && (
)} {v.dailyKm?.toLocaleString()} km
{!v.isDataSynced && 未对接}
{v.totalKm?.toLocaleString()} km
{v.isOnline ? '运行中' : '静止/离线'}
)}
{/* Ultra Compact Header - Two Rows */}
{/* Top Row: Title & Sort */}

里程看板

实时监控 • 每分钟更新
{/* Bottom Row: Quick Filters & Advanced Filter Icon */}
{/* Expandable Filter Panel */} {isFilterOpen && (
{/* Date */}
setFilterDate(e.target.value)} />
{/* Project */}
{/* Department */}
{/* Entity */}
{/* Region Code */}
{/* Mileage Range */}
setFilterMileageRange(prev => ({ ...prev, min: e.target.value }))} />
setFilterMileageRange(prev => ({ ...prev, max: e.target.value }))} />
)}
{/* Active Filter Tags */} {(() => { const tags: { label: string; onClear: () => void }[] = []; if (filterDept !== 'All') tags.push({ label: `部门: ${filterDept}`, onClear: () => setFilterDept('All') }); if (filterCustomer !== 'All') tags.push({ label: `客户: ${filterCustomer}`, onClear: () => setFilterCustomer('All') }); if (filterProject !== 'All') tags.push({ label: `项目: ${filterProject}`, onClear: () => setFilterProject('All') }); if (filterEntity !== 'All') tags.push({ label: `主体: ${filterEntity}`, onClear: () => setFilterEntity('All') }); if (filterPlate !== 'All') tags.push({ label: `车牌: ${filterPlate}`, onClear: () => setFilterPlate('All') }); if (searchTerm) tags.push({ label: `搜索: ${searchTerm}`, onClear: () => setSearchTerm('') }); if (appliedMileageRange.min) tags.push({ label: `里程≥${appliedMileageRange.min}`, onClear: () => { setFilterMileageRange(prev => ({ ...prev, min: '' })); setAppliedMileageRange(prev => ({ ...prev, min: '' })); } }); if (appliedMileageRange.max) tags.push({ label: `里程≤${appliedMileageRange.max}`, onClear: () => { setFilterMileageRange(prev => ({ ...prev, max: '' })); setAppliedMileageRange(prev => ({ ...prev, max: '' })); } }); if (filterRegionCode !== 'All') tags.push({ label: `地区: ${filterRegionCode}`, onClear: () => setFilterRegionCode('All') }); if (filterDate) tags.push({ label: `日期: ${filterDate}`, onClear: () => setFilterDate('') }); if (tags.length === 0) return null; const clearAll = () => { setFilterDept('All'); setFilterCustomer('All'); setFilterProject('All'); setFilterEntity('All'); setFilterPlate('All'); setSearchTerm(''); setFilterRegionCode('All'); setFilterMileageRange({ min: '', max: '' }); setAppliedMileageRange({ min: '', max: '' }); setFilterDate(''); }; return (
{tags.map((tag, i) => ( {tag.label} ))}
); })()} {/* Sticky header: KPI + 清单标题 */}
{sortBy === 'today' ? '今日' : '累计'}总里程
{Math.round(sortBy === 'today' ? stats.totalToday : stats.totalAll).toLocaleString()} km {sortBy === 'today' && stats.yesterdayTotal > 0 && (() => { const change = ((stats.totalToday - stats.yesterdayTotal) / stats.yesterdayTotal) * 100; const isUp = change >= 0; return {isUp ? '\u2191' : '\u2193'}{Math.abs(change).toFixed(1)}%; })()}
平均单车
{(stats.vehicleCount > 0 ? (sortBy === 'today' ? stats.totalToday : stats.totalAll) / stats.vehicleCount : 0).toFixed(0)}
km/台
监控台数
{stats.vehicleCount}
车辆详情清单 {total} 条
{/* Vehicle List */}
{filteredVehicles.map((v) => ( { navigator.clipboard.writeText(v.plate); }} >
{v.plate} {v.isOnline ? '在线' : '离线'}
{v.department ? v.department.replace('业务', '') : v.rentStatus || ''} {v.customer || '-'}
{!v.isDataSynced && (
)}
{v.dailyKm?.toLocaleString()} km
{v.totalKm?.toLocaleString()} km {!v.isDataSynced && ( 未同步 )}
))}
{filteredVehicles.length === 0 && !loadingMore && (

未找到匹配数据

)} {/* 加载更多提示 */} {loadingMore && (
加载中...
)} {!hasMore && filteredVehicles.length > 0 && (
已加载全部 {total} 条
)} {/* 哨兵元素:进入视口时触发加载更多 */}
{/* 回到顶部按钮 */} {showBackToTop && ( )} ); }