import { useState, useEffect, useCallback, useMemo, useRef } from 'react'; import { motion, AnimatePresence } from 'motion/react'; import { Truck, Filter, ChevronDown, Maximize2, Minimize2, RotateCcw, ArrowUp, ArrowDown, ChevronsUp, Download, } from 'lucide-react'; import type { MonitoringVehicle, MonitoringStats, MonitoringFilters } from './types'; import { fetchMonitoring } from './api'; import Blur from '../../components/Blur'; import PlateMultiSelect from './PlateMultiSelect'; import { exportMileageXlsx } from './xlsx-export'; import VehicleDetailModal from './VehicleDetailModal'; 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 === '__EMPTY__' ? '无值' : 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); const [fullscreenVehicles, setFullscreenVehicles] = useState([]); const [fullscreenStats, setFullscreenStats] = useState({ totalToday: 0, totalAll: 0, vehicleCount: 0, yesterdayTotal: 0 }); const [fullscreenRefresh, setFullscreenRefresh] = useState(0); const [fullscreenLoading, setFullscreenLoading] = useState(false); // New filters from image const [filterPlates, setFilterPlates] = useState([]); const [filterCustomer, setFilterCustomer] = useState('All'); const [filterProject, setFilterProject] = useState('All'); const [filterEntity, setFilterEntity] = useState('All'); const [filterRentStatus, setFilterRentStatus] = useState('All'); const [filterPlatePrefix, setFilterPlatePrefix] = useState('All'); const [filterTargetName, setFilterTargetName] = useState('All'); const [filterRegion, setFilterRegion] = useState('All'); const [filterMileageRange, setFilterMileageRange] = useState({ min: '', max: '' }); const [appliedMileageRange, setAppliedMileageRange] = useState({ min: '', max: '' }); const [exporting, setExporting] = useState(false); const [detailVehicle, setDetailVehicle] = useState(null); const [filterDate, setFilterDate] = useState(() => { const now = new Date(); if (now.getHours() < 5) now.setDate(now.getDate() - 1); return `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '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: [], rentStatuses: [], platePrefixes: [], targetNames: [], regions: [] }); const [total, setTotal] = useState(0); const [page, setPage] = useState(1); const [hasMore, setHasMore] = useState(true); const [loadingMore, setLoadingMore] = useState(false); const [pageLoading, setPageLoading] = useState(true); const [showBackToTop, setShowBackToTop] = useState(false); const PAGE_SIZE = 50; const departments = filterOptions.departments; const plateNumbers = filterOptions.plates; // 加载首页数据 const loadFirstPage = useCallback(() => { setPageLoading(true); 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, rentStatus: filterRentStatus !== 'All' ? filterRentStatus : undefined, platePrefix: filterPlatePrefix !== 'All' ? filterPlatePrefix : undefined, targetName: filterTargetName !== 'All' ? filterTargetName : undefined, region: filterRegion !== 'All' ? filterRegion : undefined, plate: filterPlates.length > 0 ? filterPlates.join(',') : 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(() => {}).finally(() => setPageLoading(false)); }, [sortBy, sortOrder, searchTerm, filterDept, filterCustomer, filterProject, filterEntity, filterRentStatus, filterPlatePrefix, filterTargetName, filterRegion, filterPlates, 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, rentStatus: filterRentStatus !== 'All' ? filterRentStatus : undefined, platePrefix: filterPlatePrefix !== 'All' ? filterPlatePrefix : undefined, targetName: filterTargetName !== 'All' ? filterTargetName : undefined, region: filterRegion !== 'All' ? filterRegion : undefined, plate: filterPlates.length > 0 ? filterPlates.join(',') : 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, filterRentStatus, filterPlatePrefix, filterTargetName, filterRegion, filterPlates, appliedMileageRange, filterDate, page, loadingMore, hasMore]); // 筛选/排序变化时重新加载 useEffect(() => { loadFirstPage(); }, [loadFirstPage]); // 区域级联:plate 选项收窄后,剔除已选但已不属于该区域的车牌 useEffect(() => { if (filterPlates.length === 0) return; const valid = new Set(filterOptions.plates); const next = filterPlates.filter(p => valid.has(p)); if (next.length !== filterPlates.length) setFilterPlates(next); }, [filterOptions.plates, filterPlates]); // 下载当前筛选结果为 xlsx const handleDownload = useCallback(async () => { if (exporting) return; setExporting(true); try { const d = await fetchMonitoring({ sortBy, sortOrder, limit: 9999, 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, rentStatus: filterRentStatus !== 'All' ? filterRentStatus : undefined, platePrefix: filterPlatePrefix !== 'All' ? filterPlatePrefix : undefined, targetName: filterTargetName !== 'All' ? filterTargetName : undefined, region: filterRegion !== 'All' ? filterRegion : undefined, plate: filterPlates.length > 0 ? filterPlates.join(',') : undefined, mileageMin: appliedMileageRange.min || undefined, mileageMax: appliedMileageRange.max || undefined, date: filterDate || undefined, }); exportMileageXlsx(d.vehicles, { date: filterDate, sortBy }); } catch (err) { console.error('export failed', err); } finally { setExporting(false); } }, [exporting, sortBy, sortOrder, searchTerm, filterDept, filterCustomer, filterProject, filterEntity, filterRentStatus, filterPlatePrefix, filterTargetName, filterRegion, filterPlates, appliedMileageRange, filterDate]); // 每分钟自动刷新 useEffect(() => { const timer = setInterval(loadFirstPage, 60 * 1000); return () => clearInterval(timer); }, [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(0, 0); document.documentElement.scrollTop = 0; document.body.scrollTop = 0; }; const filteredVehicles = vehicles; const toggleFullscreen = () => { setIsFullscreen(!isFullscreen); }; // 全屏时加载全部数据(无分页),筛选变化时重新加载 useEffect(() => { if (!isFullscreen) return; setFullscreenLoading(true); fetchMonitoring({ sortBy, sortOrder, limit: 9999, page: 1, search: searchTerm || undefined, dept: filterDept !== 'All' ? filterDept : undefined, customer: filterCustomer !== 'All' ? filterCustomer : undefined, rentStatus: filterRentStatus !== 'All' ? filterRentStatus : undefined, platePrefix: filterPlatePrefix !== 'All' ? filterPlatePrefix : undefined, targetName: filterTargetName !== 'All' ? filterTargetName : undefined, region: filterRegion !== 'All' ? filterRegion : undefined, plate: filterPlates.length > 0 ? filterPlates.join(',') : undefined, date: filterDate || undefined, }).then(d => { setFullscreenVehicles(d.vehicles); setFullscreenStats(d.stats); setFilterOptions(d.filters); }).catch(() => {}).finally(() => setFullscreenLoading(false)); }, [isFullscreen, sortBy, sortOrder, searchTerm, filterDept, filterCustomer, filterRentStatus, filterPlatePrefix, filterTargetName, filterRegion, filterPlates, filterDate, fullscreenRefresh]); // 全屏时禁止背景滚动 useEffect(() => { if (isFullscreen) { document.body.style.overflow = 'hidden'; } else { document.body.style.overflow = ''; } return () => { document.body.style.overflow = ''; }; }, [isFullscreen]); // 检测是否在小程序 webview 中(微信/抖音/支付宝等),且当前是竖屏 // 小程序 webview 无法调用系统旋转 API,只能用 CSS rotate 强制横屏 const forceLandscape = useMemo(() => { if (typeof window === 'undefined') return false; const ua = navigator.userAgent || ''; const isMiniProgram = /miniProgram/i.test(ua) || /toutiaomicroapp/i.test(ua) || /AlipayClient/i.test(ua) || (window as any).__wxjs_environment === 'miniprogram'; const isPortrait = window.innerHeight > window.innerWidth; return isMiniProgram && isPortrait; }, [isFullscreen]); return ( <> {/* 顶部哨兵:离开视口时显示回到顶部按钮 */}
{/* Fullscreen Landscape View Overlay */} {isFullscreen && ( {/* Top bar: compact inline KPI */}

全屏监控

今日 {Math.round(fullscreenStats.totalToday).toLocaleString()} km | 累计 {Math.round(fullscreenStats.totalAll).toLocaleString()} km | 车辆 {fullscreenStats.vehicleCount} | {(fullscreenStats.vehicleCount > 0 ? (sortBy === 'today' ? fullscreenStats.totalToday : fullscreenStats.totalAll) / fullscreenStats.vehicleCount : 0).toFixed(0)} km
{/* Batch selector + legend */}
{filterOptions.targetNames.map(n => ( ))}
在线 离线 未对接
{/* Table Area */}
{fullscreenLoading && (
加载中...
)} {fullscreenVehicles.map((v) => ( ))}
状态
车牌号 {filterPlates.length === 0 ? '全部' : `已选 ${filterPlates.length}`}
客户
租赁状态
部门
{ 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.customer || '-'} {v.rentStatus || '-'} {v.department || '-'} {(v.isDataSynced || v.totalKm != null) ? <>{Math.max(0, v.dailyKm || 0).toLocaleString()} km : 未对接} {v.totalKm != null ? <>{v.totalKm.toLocaleString()} km : 未对接}
)}
{/* Ultra Compact Header - Two Rows */}
{/* Top Row: Title & Sort */}

里程看板

实时监控 • 每分钟更新
{/* Bottom Row: 外部三选 (批次型号 / 运营区域 / 车牌多选) + 详情筛选 */}
{/* Expandable Filter Panel */} {isFilterOpen && (
{/* Date */}
setFilterDate(e.target.value)} />
{/* Department */}
{/* Customer */}
{/* Project */}
{/* Rent Status */}
{/* Entity */}
{/* Plate Prefix */}
{/* 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 (filterTargetName !== 'All') tags.push({ label: `批次: ${filterTargetName}`, onClear: () => setFilterTargetName('All') }); if (filterRegion !== 'All') tags.push({ label: `区域: ${filterRegion}`, onClear: () => setFilterRegion('All') }); if (filterPlates.length > 0) tags.push({ label: `车牌: ${filterPlates.length === 1 ? filterPlates[0] : `${filterPlates[0]} 等${filterPlates.length}`}`, onClear: () => setFilterPlates([]) }); if (filterRentStatus !== 'All') tags.push({ label: `状态: ${filterRentStatus}`, onClear: () => setFilterRentStatus('All') }); if (filterDept !== 'All') tags.push({ label: `部门: ${filterDept === '__EMPTY__' ? '无值' : filterDept}`, onClear: () => setFilterDept('All') }); if (filterCustomer !== 'All') tags.push({ label: `客户: ${filterCustomer === '__EMPTY__' ? '无值' : 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 (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 (filterPlatePrefix !== 'All') tags.push({ label: `车牌段: ${filterPlatePrefix}`, onClear: () => setFilterPlatePrefix('All') }); if (filterDate) tags.push({ label: `日期: ${filterDate}`, onClear: () => setFilterDate('') }); if (tags.length === 0) return null; const clearAll = () => { setFilterDept('All'); setFilterCustomer('All'); setFilterRentStatus('All'); setFilterProject('All'); setFilterEntity('All'); setFilterPlates([]); setSearchTerm(''); setFilterPlatePrefix('All'); setFilterTargetName('All'); setFilterRegion('All'); setFilterMileageRange({ min: '', max: '' }); setAppliedMileageRange({ min: '', max: '' }); setFilterDate(''); }; return (
{tags.map((tag, i) => ( {tag.label} ))}
); })()} {/* Sticky header: KPI + 清单标题 */}
{sortBy === 'today' ? '今日' : '累计'}总里程
{pageLoading ?
: <>{Math.round(sortBy === 'today' ? stats.totalToday : stats.totalAll).toLocaleString()} km}
平均单车
{pageLoading ?
: (stats.vehicleCount > 0 ? (sortBy === 'today' ? stats.totalToday : stats.totalAll) / stats.vehicleCount : 0).toFixed(0)}
km/台
监控台数
{pageLoading ?
: stats.vehicleCount}
车辆详情清单 {total} 条
{/* Vehicle List */}
{pageLoading && (
{Array.from({ length: 6 }).map((_, i) => (
))}
)}
{filteredVehicles.map((v) => ( setDetailVehicle(v)} >
{v.plate} {v.isOnline ? '在线' : '离线'}
{v.rentStatus || ''}{v.department ? ` · ${v.department.replace('业务', '')}` : ''} {v.customer || '-'}
{!v.isDataSynced && v.totalKm == null && (
)}
{(v.isDataSynced || v.totalKm != null) ? <>{Math.max(0, v.dailyKm || 0).toLocaleString()} km : 未对接}
{v.totalKm != null ? `${v.totalKm.toLocaleString()} km` : '未对接'}
))}
{filteredVehicles.length === 0 && !loadingMore && (

未找到匹配数据

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