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, Check, CalendarDays,
RefreshCw, ArrowLeftRight, Power,
} from 'lucide-react';
import { BarChart, Bar, ResponsiveContainer, Tooltip, ReferenceLine, XAxis } from 'recharts';
import type {
MileageSourceGroup,
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 HIGH_MILEAGE_ALERT_TARGETS = new Set([
'交投40辆4.5T普货',
'交投190辆4.5T冷链车',
]);
const HIGH_MILEAGE_ALERT_KM = 800;
const ALL_MILEAGE_SOURCE_GROUPS: MileageSourceGroup[] = ['instrument', 'gps'];
const MILEAGE_SOURCE_META = {
instrument: { label: '仪表数据', protocol: '32960 > MQTT' },
gps: { label: 'GPS数据', protocol: 'JT808' },
} as const;
function vehicleSourceDisplay(vehicle: MonitoringVehicle): {
label: string;
title: string;
className: string;
} {
if (vehicle.sourceCategory === 'INSTRUMENT') {
return {
label: '仪表数据',
title: vehicle.sourceProtocol || 'GB32960 / MQTT',
className: 'bg-violet-50 text-violet-600',
};
}
if (vehicle.sourceCategory === 'GPS') {
return {
label: 'GPS数据',
title: vehicle.sourceProtocol || 'JT808',
className: 'bg-emerald-50 text-emerald-600',
};
}
if (vehicle.sourceCategory === 'MIXED') {
return {
label: '仪表+GPS',
title: '所选区间内包含仪表数据和 GPS 数据',
className: 'bg-blue-50 text-blue-600',
};
}
return {
label: vehicle.isDataSynced ? '来源待接口' : '无数据',
title: vehicle.isDataSynced
? 'OneOS 当前响应未返回 sourceProtocol'
: '所选来源没有有效数据',
className: vehicle.isDataSynced
? 'bg-amber-50 text-amber-600'
: 'bg-slate-100 text-slate-400',
};
}
function SourcePriorityControl({
priority,
onChange,
}: {
priority: MileageSourceGroup[];
onChange: (next: MileageSourceGroup[]) => void;
}) {
const ordered = [
...priority,
...ALL_MILEAGE_SOURCE_GROUPS.filter(group => !priority.includes(group)),
];
const toggle = (group: MileageSourceGroup) => {
if (priority.includes(group)) {
if (priority.length === 1) return;
onChange(priority.filter(item => item !== group));
return;
}
onChange([...priority, group]);
};
const swap = () => {
if (priority.length === 2) onChange([priority[1], priority[0]]);
};
return (
数据来源
{ordered.map(group => {
const enabledIndex = priority.indexOf(group);
const enabled = enabledIndex >= 0;
const meta = MILEAGE_SOURCE_META[group];
return (
);
})}
);
}
function defaultMileageDate(): string {
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')}`;
}
function parseOneOsTime(value: string | null | undefined): number | null {
if (!value) return null;
let normalized = value.trim().replace(' ', 'T');
if (!/(?:Z|[+-]\d{2}:?\d{2})$/i.test(normalized)) normalized += '+08:00';
const timestamp = Date.parse(normalized);
return Number.isFinite(timestamp) ? timestamp : null;
}
function formatAbsoluteTime(value: string | null | undefined): string | null {
const timestamp = parseOneOsTime(value);
if (timestamp === null) return null;
return new Intl.DateTimeFormat('zh-CN', {
timeZone: 'Asia/Shanghai',
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hour12: false,
}).format(new Date(timestamp));
}
function vehicleStatisticTime(vehicle: MonitoringVehicle, nowMs: number): {
label: string;
title: string;
color: string;
} {
const primaryTime = vehicle.dataTime || vehicle.updatedAt || vehicle.calculatedAt;
const timestamp = parseOneOsTime(primaryTime);
if (timestamp === null) {
return {
label: vehicle.isDataSynced ? '统计时间待接口' : '无有效数据时间',
title: 'OneOS 当前未返回车辆级数据时间',
color: vehicle.isDataSynced ? 'text-amber-500' : 'text-slate-500',
};
}
const deltaSeconds = Math.floor((nowMs - timestamp) / 1000);
let label: string;
if (deltaSeconds < -60) label = '时间异常';
else if (deltaSeconds < 5) label = '刚刚';
else if (deltaSeconds < 60) label = `${deltaSeconds}秒前`;
else if (deltaSeconds < 3600) label = `${Math.floor(deltaSeconds / 60)}分钟前`;
else if (deltaSeconds < 86400) label = `${Math.floor(deltaSeconds / 3600)}小时前`;
else label = `${Math.floor(deltaSeconds / 86400)}天前`;
const absoluteDataTime = formatAbsoluteTime(vehicle.dataTime);
const absoluteCalculatedAt = formatAbsoluteTime(vehicle.calculatedAt);
const absoluteUpdatedAt = formatAbsoluteTime(vehicle.updatedAt);
const title = [
absoluteDataTime ? `数据时间:${absoluteDataTime}` : null,
absoluteCalculatedAt ? `计算时间:${absoluteCalculatedAt}` : null,
absoluteUpdatedAt ? `更新时间:${absoluteUpdatedAt}` : null,
].filter(Boolean).join('\n');
const ageSeconds = Math.max(0, deltaSeconds);
const color = ageSeconds <= 300
? 'text-emerald-500'
: ageSeconds <= 3600
? 'text-blue-500'
: ageSeconds <= 86400
? 'text-amber-500'
: 'text-rose-500';
return { label, title, color };
}
function normalizeRangeLabel(start: string, end: string): string {
if (!start && !end) return '最新数据';
if (start && end && start === end) return start;
return `${start || end} 至 ${end || start}`;
}
function fmtDate(date: Date): string {
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`;
}
type RangePreset = 'today' | 'yesterday' | 'thisWeek' | 'thisMonth' | 'last7' | 'last15';
function getRangePreset(preset: RangePreset): { start: string; end: string } {
const end = new Date(`${defaultMileageDate()}T00:00:00`);
const start = new Date(end);
if (preset === 'yesterday') {
start.setDate(start.getDate() - 1);
end.setDate(end.getDate() - 1);
} else if (preset === 'thisWeek') {
const day = start.getDay() || 7;
start.setDate(start.getDate() - day + 1);
} else if (preset === 'thisMonth') {
start.setDate(1);
} else if (preset === 'last7') {
start.setDate(start.getDate() - 6);
} else if (preset === 'last15') {
start.setDate(start.getDate() - 14);
}
return { start: fmtDate(start), end: fmtDate(end) };
}
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 && (
无匹配项
)}
)}
);
};
const BatchMultiSelect = ({
options,
selected,
onChange,
placeholder,
}: {
options: string[],
selected: string[],
onChange: (val: string[]) => void,
placeholder: string
}) => {
const rootRef = useRef(null);
const [isOpen, setIsOpen] = useState(false);
const [search, setSearch] = useState('');
const selectedSet = useMemo(() => new Set(selected), [selected]);
const filtered = useMemo(() => {
if (!search) return options;
return options.filter(opt => opt.toLowerCase().includes(search.toLowerCase()));
}, [options, search]);
const label = selected.length === 0
? placeholder
: selected.length === options.length
? '全部批次'
: selected.length === 1
? selected[0]
: `已选 ${selected.length} 个批次`;
const toggle = (opt: string) => {
if (selectedSet.has(opt)) {
onChange(selected.filter(item => item !== opt));
} else {
onChange([...selected, opt]);
}
};
useEffect(() => {
if (!isOpen) return;
const handlePointerDown = (event: PointerEvent) => {
const target = event.target;
if (target instanceof Node && !rootRef.current?.contains(target)) {
setIsOpen(false);
setSearch('');
}
};
document.addEventListener('pointerdown', handlePointerDown);
return () => document.removeEventListener('pointerdown', handlePointerDown);
}, [isOpen]);
return (
{isOpen && (
setSearch(e.target.value)}
/>
{filtered.map((opt: string) => {
const checked = selectedSet.has(opt);
return (
);
})}
{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 [filterTargetNames, setFilterTargetNames] = useState([]);
const [filterRegion, setFilterRegion] = useState('All');
const [filterBrands, setFilterBrands] = useState([]);
const [filterMileageRange, setFilterMileageRange] = useState({ min: '', max: '' });
const [appliedMileageRange, setAppliedMileageRange] = useState({ min: '', max: '' });
const [exporting, setExporting] = useState(false);
const [detailVehicle, setDetailVehicle] = useState(null);
const [rangeStart, setRangeStart] = useState(defaultMileageDate);
const [rangeEnd, setRangeEnd] = useState(defaultMileageDate);
const [sourcePriority, setSourcePriority] = useState(['instrument', 'gps']);
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: [], brands: [] });
const [rangeDailyTotals, setRangeDailyTotals] = useState<{ date: string; totalKm: number }[]>([]);
const [effectiveRange, setEffectiveRange] = useState<{ start: string; end: string }>(() => ({ start: defaultMileageDate(), end: defaultMileageDate() }));
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 [manualRefreshing, setManualRefreshing] = useState(false);
const [lastRefreshedAt, setLastRefreshedAt] = useState(null);
const [showBackToTop, setShowBackToTop] = useState(false);
const [relativeNow, setRelativeNow] = useState(() => Date.now());
const PAGE_SIZE = 50;
useEffect(() => {
const timer = setInterval(() => setRelativeNow(Date.now()), 5000);
return () => clearInterval(timer);
}, []);
const departments = filterOptions.departments;
const plateNumbers = filterOptions.plates;
const rangeLabel = normalizeRangeLabel(effectiveRange.start, effectiveRange.end);
const isRangeMode = !!effectiveRange.start && !!effectiveRange.end && effectiveRange.start !== effectiveRange.end;
const averageDailyKm = rangeDailyTotals.length > 0
? rangeDailyTotals.reduce((sum, item) => sum + item.totalKm, 0) / rangeDailyTotals.length
: 0;
const applyRangePreset = useCallback((preset: RangePreset) => {
const range = getRangePreset(preset);
setRangeStart(range.start);
setRangeEnd(range.end);
}, []);
const isHighMileageAlert = useCallback((v: MonitoringVehicle) => {
const inAlertTarget = v.targetNames?.some(name => HIGH_MILEAGE_ALERT_TARGETS.has(name))
|| filterTargetNames.some(name => HIGH_MILEAGE_ALERT_TARGETS.has(name));
return inAlertTarget && Math.max(0, v.dailyKm || 0) >= HIGH_MILEAGE_ALERT_KM;
}, [filterTargetNames]);
// 加载首页数据
const loadFirstPage = useCallback((showPageLoading = true) => {
if (showPageLoading) setPageLoading(true);
return 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,
targetNames: filterTargetNames.length > 0 ? filterTargetNames : undefined,
region: filterRegion !== 'All' ? filterRegion : undefined,
plate: filterPlates.length > 0 ? filterPlates.join(',') : undefined,
mileageMin: appliedMileageRange.min || undefined,
mileageMax: appliedMileageRange.max || undefined,
startDate: rangeStart || undefined,
endDate: rangeEnd || undefined,
brands: filterBrands.length > 0 ? filterBrands : undefined,
sourcePriority,
}).then(d => {
setVehicles(d.vehicles);
setStats(d.stats);
setFilterOptions(d.filters);
setRangeDailyTotals(d.rangeDailyTotals || []);
setEffectiveRange(d.dateRange || { start: rangeStart, end: rangeEnd });
setTotal(d.total);
setPage(1);
setHasMore(d.page < d.totalPages);
setLastRefreshedAt(new Date());
}).catch(() => {}).finally(() => {
if (showPageLoading) setPageLoading(false);
});
}, [sortBy, sortOrder, searchTerm, filterDept, filterCustomer, filterProject, filterEntity, filterRentStatus, filterPlatePrefix, filterTargetNames, filterRegion, filterPlates, appliedMileageRange, rangeStart, rangeEnd, filterBrands, sourcePriority]);
const handleManualRefresh = useCallback(async () => {
if (manualRefreshing) return;
setManualRefreshing(true);
try {
await loadFirstPage(false);
} finally {
setManualRefreshing(false);
}
}, [loadFirstPage, manualRefreshing]);
// 加载更多
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,
targetNames: filterTargetNames.length > 0 ? filterTargetNames : undefined,
region: filterRegion !== 'All' ? filterRegion : undefined,
plate: filterPlates.length > 0 ? filterPlates.join(',') : undefined,
mileageMin: appliedMileageRange.min || undefined,
mileageMax: appliedMileageRange.max || undefined,
startDate: rangeStart || undefined,
endDate: rangeEnd || undefined,
brands: filterBrands.length > 0 ? filterBrands : undefined,
sourcePriority,
}).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, filterTargetNames, filterRegion, filterPlates, appliedMileageRange, rangeStart, rangeEnd, page, loadingMore, hasMore, filterBrands, sourcePriority]);
// 筛选/排序变化时重新加载
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,
targetNames: filterTargetNames.length > 0 ? filterTargetNames : undefined,
region: filterRegion !== 'All' ? filterRegion : undefined,
plate: filterPlates.length > 0 ? filterPlates.join(',') : undefined,
mileageMin: appliedMileageRange.min || undefined,
mileageMax: appliedMileageRange.max || undefined,
startDate: rangeStart || undefined,
endDate: rangeEnd || undefined,
brands: filterBrands.length > 0 ? filterBrands : undefined,
sourcePriority,
});
exportMileageXlsx(d.vehicles, { startDate: d.dateRange?.start || rangeStart, endDate: d.dateRange?.end || rangeEnd, sortBy });
} catch (err) {
console.error('export failed', err);
} finally {
setExporting(false);
}
}, [exporting, sortBy, sortOrder, searchTerm, filterDept, filterCustomer, filterProject, filterEntity, filterRentStatus, filterPlatePrefix, filterTargetNames, filterRegion, filterPlates, appliedMileageRange, rangeStart, rangeEnd, filterBrands, sourcePriority]);
// 每分钟自动刷新
useEffect(() => {
const timer = setInterval(() => { void loadFirstPage(false); }, 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,
targetNames: filterTargetNames.length > 0 ? filterTargetNames : undefined,
region: filterRegion !== 'All' ? filterRegion : undefined,
plate: filterPlates.length > 0 ? filterPlates.join(',') : undefined,
startDate: rangeStart || undefined,
endDate: rangeEnd || undefined,
brands: filterBrands.length > 0 ? filterBrands : undefined,
sourcePriority,
}).then(d => {
setFullscreenVehicles(d.vehicles);
setFullscreenStats(d.stats);
setFilterOptions(d.filters);
}).catch(() => {}).finally(() => setFullscreenLoading(false));
}, [isFullscreen, sortBy, sortOrder, searchTerm, filterDept, filterCustomer, filterRentStatus, filterPlatePrefix, filterTargetNames, filterRegion, filterPlates, rangeStart, rangeEnd, fullscreenRefresh, filterBrands, sourcePriority]);
// 全屏时禁止背景滚动
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
|
来源
{sourcePriority.map(group => MILEAGE_SOURCE_META[group].label).join(' > ')}
{/* Batch selector + legend */}
{filterOptions.targetNames.map(n => (
))}
在线
离线
未对接
{/* Table Area */}
{fullscreenLoading && (
)}
| 状态 |
车牌号
{filterPlates.length === 0 ? '全部' : `已选 ${filterPlates.length}`}
|
客户
{
const val = e.target.value.trim();
setFilterCustomer(val || 'All');
e.target.value = '';
}}
onKeyDown={(e) => {
if (e.key === 'Enter') {
const val = (e.target as HTMLInputElement).value.trim();
setFilterCustomer(val || 'All');
(e.target as HTMLInputElement).value = '';
}
}}
/>
|
品牌
{
const val = e.target.value.trim();
if (val) {
setFilterBrands(filterBrands.includes(val) ? filterBrands.filter(b => b !== val) : [...filterBrands, val]);
}
e.target.value = '';
}}
onKeyDown={(e) => {
if (e.key === 'Enter') {
const val = (e.target as HTMLInputElement).value.trim();
if (val) {
setFilterBrands(filterBrands.includes(val) ? filterBrands.filter(b => b !== val) : [...filterBrands, val]);
}
(e.target as HTMLInputElement).value = '';
}
}}
/>
|
租赁状态
|
部门
|
{
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) => {
const highMileageAlert = isHighMileageAlert(v);
const statisticTime = vehicleStatisticTime(v, relativeNow);
const sourceDisplay = vehicleSourceDisplay(v);
return (
|
|
{v.plate}
{statisticTime.label}
{sourceDisplay.label}
|
{v.customer || '-'} |
{v.brand || '-'} |
{v.rentStatus || '-'} |
{v.department || '-'} |
{(v.isDataSynced || v.totalKm != null) ? <>{Math.max(0, v.dailyKm || 0).toLocaleString()} km> : 未对接}
|
{v.totalKm != null ? <>{v.totalKm.toLocaleString()} km> : 未对接}
|
);
})}
)}
里程看板
数据监控 • OneOS 实时查询
{lastRefreshedAt ? lastRefreshedAt.toLocaleTimeString('zh-CN', { hour12: false }) : '--:--:--'}
{([
['today', '今天'],
['yesterday', '昨天'],
['thisWeek', '本周'],
['thisMonth', '本月'],
['last15', '近15天'],
] as Array<[RangePreset, string]>).map(([preset, label]) => {
const range = getRangePreset(preset);
const active = rangeStart === range.start && rangeEnd === range.end;
return (
);
})}
{rangeLabel}
{/* Expandable Filter Panel */}
{isFilterOpen && (
{/* Date range */}
setRangeStart(e.target.value)}
/>
至
setRangeEnd(e.target.value)}
/>
{([
['today', '今天'],
['yesterday', '昨天'],
['thisWeek', '本周'],
['thisMonth', '本月'],
['last7', '近7天'],
['last15', '近15天'],
] as Array<[RangePreset, string]>).map(([preset, label]) => (
))}
{/* Department */}
{/* Customer */}
{/* Brand multi-select */}
{/* Project */}
{/* Rent Status */}
{/* Entity */}
{/* Plate Prefix */}
{/* Mileage Range */}
)}
{/* Active Filter Tags */}
{(() => {
const tags: { label: string; onClear: () => void }[] = [];
if (filterTargetNames.length > 0) tags.push({
label: filterTargetNames.length === filterOptions.targetNames.length ? '批次: 全部批次' : `批次: ${filterTargetNames.length === 1 ? filterTargetNames[0] : `${filterTargetNames[0]} 等${filterTargetNames.length}`}`,
onClear: () => setFilterTargetNames([])
});
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 (filterBrands.length > 0) tags.push({
label: `品牌: ${filterBrands.length === 1 ? filterBrands[0] : `${filterBrands[0]} 等${filterBrands.length}`}`,
onClear: () => setFilterBrands([])
});
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 (rangeStart || rangeEnd) tags.push({
label: `区间: ${normalizeRangeLabel(rangeStart, rangeEnd)}`,
onClear: () => {
const today = defaultMileageDate();
setRangeStart(today);
setRangeEnd(today);
}
});
if (tags.length === 0) return null;
const clearAll = () => {
setFilterDept('All'); setFilterCustomer('All'); setFilterRentStatus('All'); setFilterProject('All'); setFilterEntity('All');
setFilterPlates([]); setSearchTerm(''); setFilterPlatePrefix('All'); setFilterTargetNames([]); setFilterRegion('All');
setFilterMileageRange({ min: '', max: '' }); setAppliedMileageRange({ min: '', max: '' });
setFilterBrands([]);
const today = defaultMileageDate();
setRangeStart(today); setRangeEnd(today);
};
return (
已选条件
{tags.map((tag, i) => (
{tag.label}
))}
);
})()}
{/* Sticky header: KPI + 清单标题 */}
{sortBy === 'today' ? (isRangeMode ? '区间' : '当日') : '累计'}总里程
{pageLoading ?
: <>{Math.round(sortBy === 'today' ? stats.totalToday : stats.totalAll).toLocaleString()}
km>}
{rangeLabel}
平均单车
{pageLoading ?
: (stats.vehicleCount > 0 ? (sortBy === 'today' ? stats.totalToday : stats.totalAll) / stats.vehicleCount : 0).toFixed(0)}
km/台
监控台数
{pageLoading ?
: stats.vehicleCount}
台
{pageLoading ? (
) : isRangeMode ? (
区间走势
{rangeDailyTotals.length} 天 · km
{rangeDailyTotals.length === 0 ? (
暂无趋势
) : (
[`${Number(value ?? 0).toLocaleString('zh-CN', { maximumFractionDigits: 2 })} km`, '当日里程']}
labelFormatter={(label) => `日期 ${String(label)}`}
contentStyle={{ borderRadius: 10, borderColor: '#e2e8f0', fontSize: 11 }}
cursor={{ fill: 'rgba(37, 99, 235, 0.06)' }}
/>
{averageDailyKm > 0 && }
)}
日均
{Math.round(averageDailyKm).toLocaleString()}
km
) : (
单日概览
{rangeLabel} · 单位 km
当日总计
{Math.round(stats.totalToday).toLocaleString()}
日均单车
{stats.vehicleCount > 0 ? Math.round(stats.totalToday / stats.vehicleCount).toLocaleString() : 0}
)}
车辆详情清单
{total} 条
车牌 / 数据状态
客户 / 归属
{isRangeMode ? '区间里程' : '当日里程'}
累计里程
{/* Vehicle List */}
{pageLoading && (
{Array.from({ length: 6 }).map((_, i) => (
))}
)}
{filteredVehicles.map((v) => {
const highMileageAlert = isHighMileageAlert(v);
const statisticTime = vehicleStatisticTime(v, relativeNow);
const sourceDisplay = vehicleSourceDisplay(v);
return (
setDetailVehicle(v)}
>
{v.plate}
{v.isOnline ? '在线' : '离线'}
{statisticTime.label}
{sourceDisplay.label}
{v.rentStatus || ''}{v.department ? ` · ${v.department.replace('业务', '')}` : ''}
{v.customer || '-'}
{v.customer || '-'}
{[v.rentStatus, v.department?.replace('业务', ''), v.project].filter(Boolean).join(' · ') || '暂无归属信息'}
{Math.max(0, v.dailyKm || 0).toLocaleString()}
km
{v.totalKm != null ? v.totalKm.toLocaleString() : '-'}
km
{!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)}
sourcePriority={sourcePriority}
/>
{/* 回到顶部按钮 */}
{showBackToTop && (
)}
>
);
}