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 && (
)}
| 状态 |
车牌号
{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' ? :
)}
|
{fullscreenVehicles.map((v) => (
|
|
{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 */}
)}
{/* 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 && (
)}
>
);
}