feat: polish BI dashboards and bump version
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
This commit is contained in:
@@ -3,8 +3,9 @@ import { motion, AnimatePresence } from 'motion/react';
|
||||
import {
|
||||
Truck, Filter, ChevronDown,
|
||||
Maximize2, Minimize2, RotateCcw,
|
||||
ArrowUp, ArrowDown, ChevronsUp, Download, Check,
|
||||
ArrowUp, ArrowDown, ChevronsUp, Download, Check, CalendarDays,
|
||||
} from 'lucide-react';
|
||||
import { BarChart, Bar, ResponsiveContainer, Tooltip, ReferenceLine } from 'recharts';
|
||||
import type { MonitoringVehicle, MonitoringStats, MonitoringFilters } from './types';
|
||||
import { fetchMonitoring } from './api';
|
||||
import Blur from '../../components/Blur';
|
||||
@@ -18,6 +19,40 @@ const HIGH_MILEAGE_ALERT_TARGETS = new Set([
|
||||
]);
|
||||
const HIGH_MILEAGE_ALERT_KM = 800;
|
||||
|
||||
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 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' | '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 === '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,
|
||||
@@ -246,15 +281,14 @@ export default function MonitoringView() {
|
||||
const [appliedMileageRange, setAppliedMileageRange] = useState({ min: '', max: '' });
|
||||
const [exporting, setExporting] = useState(false);
|
||||
const [detailVehicle, setDetailVehicle] = useState<MonitoringVehicle | null>(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 [rangeStart, setRangeStart] = useState(defaultMileageDate);
|
||||
const [rangeEnd, setRangeEnd] = useState(defaultMileageDate);
|
||||
|
||||
const [vehicles, setVehicles] = useState<MonitoringVehicle[]>([]);
|
||||
const [stats, setStats] = useState<MonitoringStats>({ totalToday: 0, totalAll: 0, vehicleCount: 0, yesterdayTotal: 0 });
|
||||
const [filterOptions, setFilterOptions] = useState<MonitoringFilters>({ departments: [], customers: [], plates: [], projects: [], entities: [], rentStatuses: [], platePrefixes: [], targetNames: [], regions: [] });
|
||||
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);
|
||||
@@ -265,6 +299,20 @@ export default function MonitoringView() {
|
||||
|
||||
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 topLoadedVehicle = useMemo(
|
||||
() => vehicles.reduce<MonitoringVehicle | null>((best, vehicle) => (!best || vehicle.dailyKm > best.dailyKm ? vehicle : best), null),
|
||||
[vehicles],
|
||||
);
|
||||
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))
|
||||
@@ -292,16 +340,19 @@ export default function MonitoringView() {
|
||||
plate: filterPlates.length > 0 ? filterPlates.join(',') : undefined,
|
||||
mileageMin: appliedMileageRange.min || undefined,
|
||||
mileageMax: appliedMileageRange.max || undefined,
|
||||
date: filterDate || undefined,
|
||||
startDate: rangeStart || undefined,
|
||||
endDate: rangeEnd || undefined,
|
||||
}).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);
|
||||
}).catch(() => {}).finally(() => setPageLoading(false));
|
||||
}, [sortBy, sortOrder, searchTerm, filterDept, filterCustomer, filterProject, filterEntity, filterRentStatus, filterPlatePrefix, filterTargetNames, filterRegion, filterPlates, appliedMileageRange, filterDate]);
|
||||
}, [sortBy, sortOrder, searchTerm, filterDept, filterCustomer, filterProject, filterEntity, filterRentStatus, filterPlatePrefix, filterTargetNames, filterRegion, filterPlates, appliedMileageRange, rangeStart, rangeEnd]);
|
||||
|
||||
// 加载更多
|
||||
const loadMore = useCallback(() => {
|
||||
@@ -325,13 +376,14 @@ export default function MonitoringView() {
|
||||
plate: filterPlates.length > 0 ? filterPlates.join(',') : undefined,
|
||||
mileageMin: appliedMileageRange.min || undefined,
|
||||
mileageMax: appliedMileageRange.max || undefined,
|
||||
date: filterDate || undefined,
|
||||
startDate: rangeStart || undefined,
|
||||
endDate: rangeEnd || 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, filterTargetNames, filterRegion, filterPlates, appliedMileageRange, filterDate, page, loadingMore, hasMore]);
|
||||
}, [sortBy, sortOrder, searchTerm, filterDept, filterCustomer, filterProject, filterEntity, filterRentStatus, filterPlatePrefix, filterTargetNames, filterRegion, filterPlates, appliedMileageRange, rangeStart, rangeEnd, page, loadingMore, hasMore]);
|
||||
|
||||
// 筛选/排序变化时重新加载
|
||||
useEffect(() => {
|
||||
@@ -368,15 +420,16 @@ export default function MonitoringView() {
|
||||
plate: filterPlates.length > 0 ? filterPlates.join(',') : undefined,
|
||||
mileageMin: appliedMileageRange.min || undefined,
|
||||
mileageMax: appliedMileageRange.max || undefined,
|
||||
date: filterDate || undefined,
|
||||
startDate: rangeStart || undefined,
|
||||
endDate: rangeEnd || undefined,
|
||||
});
|
||||
exportMileageXlsx(d.vehicles, { date: filterDate, sortBy });
|
||||
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, filterDate]);
|
||||
}, [exporting, sortBy, sortOrder, searchTerm, filterDept, filterCustomer, filterProject, filterEntity, filterRentStatus, filterPlatePrefix, filterTargetNames, filterRegion, filterPlates, appliedMileageRange, rangeStart, rangeEnd]);
|
||||
|
||||
// 每分钟自动刷新
|
||||
useEffect(() => {
|
||||
@@ -445,13 +498,14 @@ export default function MonitoringView() {
|
||||
targetNames: filterTargetNames.length > 0 ? filterTargetNames : undefined,
|
||||
region: filterRegion !== 'All' ? filterRegion : undefined,
|
||||
plate: filterPlates.length > 0 ? filterPlates.join(',') : undefined,
|
||||
date: filterDate || undefined,
|
||||
startDate: rangeStart || undefined,
|
||||
endDate: rangeEnd || undefined,
|
||||
}).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, filterDate, fullscreenRefresh]);
|
||||
}, [isFullscreen, sortBy, sortOrder, searchTerm, filterDept, filterCustomer, filterRentStatus, filterPlatePrefix, filterTargetNames, filterRegion, filterPlates, rangeStart, rangeEnd, fullscreenRefresh]);
|
||||
|
||||
// 全屏时禁止背景滚动
|
||||
useEffect(() => {
|
||||
@@ -512,7 +566,7 @@ export default function MonitoringView() {
|
||||
<h2 className="text-white font-bold text-xs">全屏监控</h2>
|
||||
</div>
|
||||
<div className="flex items-center gap-3 text-[10px]">
|
||||
<span className="text-slate-500">今日 <span className="text-white font-black">{Math.round(fullscreenStats.totalToday).toLocaleString()}</span> <span className="text-blue-400">km</span></span>
|
||||
<span className="text-slate-500">区间 <span className="text-white font-black">{Math.round(fullscreenStats.totalToday).toLocaleString()}</span> <span className="text-blue-400">km</span></span>
|
||||
<span className="text-slate-700">|</span>
|
||||
<span className="text-slate-500">累计 <span className="text-white font-black">{Math.round(fullscreenStats.totalAll).toLocaleString()}</span> <span className="text-blue-400">km</span></span>
|
||||
<span className="text-slate-700">|</span>
|
||||
@@ -633,7 +687,7 @@ export default function MonitoringView() {
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center justify-end gap-1">
|
||||
<span>今日里程</span>
|
||||
<span>区间里程</span>
|
||||
{sortBy === 'today' && (
|
||||
sortOrder === 'desc' ? <ArrowDown size={10} /> : <ArrowUp size={10} />
|
||||
)}
|
||||
@@ -730,7 +784,7 @@ export default function MonitoringView() {
|
||||
onClick={() => setSortBy('today')}
|
||||
className={`px-2 py-1 text-[9px] font-bold rounded-md transition-all ${sortBy === 'today' ? 'bg-white text-blue-600 shadow-sm' : 'text-slate-400'}`}
|
||||
>
|
||||
今日
|
||||
区间
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setSortBy('total')}
|
||||
@@ -779,6 +833,35 @@ export default function MonitoringView() {
|
||||
<Filter size={16} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-1.5 overflow-x-auto no-scrollbar">
|
||||
{([
|
||||
['today', '今天'],
|
||||
['thisWeek', '本周'],
|
||||
['thisMonth', '本月'],
|
||||
['last15', '近15天'],
|
||||
] as Array<[RangePreset, string]>).map(([preset, label]) => {
|
||||
const range = getRangePreset(preset);
|
||||
const active = rangeStart === range.start && rangeEnd === range.end;
|
||||
return (
|
||||
<button
|
||||
key={preset}
|
||||
type="button"
|
||||
onClick={() => applyRangePreset(preset)}
|
||||
className={`shrink-0 rounded-lg border px-2.5 py-1.5 text-[10px] font-black transition-all ${
|
||||
active
|
||||
? 'border-blue-200 bg-blue-50 text-blue-600 shadow-sm'
|
||||
: 'border-slate-100 bg-slate-50 text-slate-500 hover:border-blue-100 hover:bg-blue-50 hover:text-blue-600'
|
||||
}`}
|
||||
>
|
||||
{label}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
<span className="ml-auto shrink-0 rounded-lg bg-slate-50 px-2 py-1 text-[10px] font-bold text-slate-400">
|
||||
{rangeLabel}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Expandable Filter Panel */}
|
||||
@@ -791,15 +874,42 @@ export default function MonitoringView() {
|
||||
className="overflow-hidden"
|
||||
>
|
||||
<div className="bg-white p-4 rounded-2xl border border-gray-100 shadow-sm mb-2 space-y-4">
|
||||
{/* Date */}
|
||||
{/* Date range */}
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-[10px] font-bold text-slate-400 uppercase tracking-wider">查询日期</label>
|
||||
<input
|
||||
type="date"
|
||||
className="w-full bg-slate-50 border-none rounded-xl py-2 px-3 text-xs focus:ring-2 focus:ring-blue-500/20 outline-none"
|
||||
value={filterDate}
|
||||
onChange={(e) => setFilterDate(e.target.value)}
|
||||
/>
|
||||
<label className="text-[10px] font-bold text-slate-400 uppercase tracking-wider">查询时间区间</label>
|
||||
<div className="grid grid-cols-[1fr_auto_1fr] items-center gap-2">
|
||||
<input
|
||||
type="date"
|
||||
className="w-full bg-slate-50 border-none rounded-xl py-2 px-3 text-xs focus:ring-2 focus:ring-blue-500/20 outline-none"
|
||||
value={rangeStart}
|
||||
onChange={(e) => setRangeStart(e.target.value)}
|
||||
/>
|
||||
<span className="text-[10px] font-bold text-slate-300">至</span>
|
||||
<input
|
||||
type="date"
|
||||
className="w-full bg-slate-50 border-none rounded-xl py-2 px-3 text-xs focus:ring-2 focus:ring-blue-500/20 outline-none"
|
||||
value={rangeEnd}
|
||||
onChange={(e) => setRangeEnd(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5 pt-1">
|
||||
{([
|
||||
['today', '今天'],
|
||||
['thisWeek', '本周'],
|
||||
['thisMonth', '本月'],
|
||||
['last7', '近7天'],
|
||||
['last15', '近15天'],
|
||||
] as Array<[RangePreset, string]>).map(([preset, label]) => (
|
||||
<button
|
||||
key={preset}
|
||||
type="button"
|
||||
className="rounded-lg bg-slate-50 px-2 py-1 text-[10px] font-bold text-slate-500 hover:bg-blue-50 hover:text-blue-600"
|
||||
onClick={() => applyRangePreset(preset)}
|
||||
>
|
||||
{label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
@@ -926,6 +1036,9 @@ export default function MonitoringView() {
|
||||
setFilterRegion('All');
|
||||
setFilterMileageRange({ min: '', max: '' });
|
||||
setAppliedMileageRange({ min: '', max: '' });
|
||||
const today = defaultMileageDate();
|
||||
setRangeStart(today);
|
||||
setRangeEnd(today);
|
||||
}}
|
||||
className="text-[10px] font-bold text-slate-400 hover:text-slate-600"
|
||||
>
|
||||
@@ -964,13 +1077,21 @@ export default function MonitoringView() {
|
||||
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 (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: '' });
|
||||
setFilterDate('');
|
||||
const today = defaultMileageDate();
|
||||
setRangeStart(today); setRangeEnd(today);
|
||||
};
|
||||
return (
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
@@ -988,13 +1109,14 @@ export default function MonitoringView() {
|
||||
})()}
|
||||
|
||||
{/* Sticky header: KPI + 清单标题 */}
|
||||
<div className="sticky top-[44px] z-20 bg-[#F8F9FB] pt-1 pb-1 space-y-2">
|
||||
<div className="sticky top-[44px] z-20 bg-[var(--app-bg)] pt-1 pb-1 space-y-2">
|
||||
<div className={`grid grid-cols-4 gap-2 transition-opacity ${pageLoading ? 'opacity-60' : ''}`}>
|
||||
<div className="col-span-2 bg-slate-900 p-2.5 rounded-xl text-white relative overflow-hidden">
|
||||
<div className="text-[7px] font-bold text-slate-500 uppercase tracking-wider">{sortBy === 'today' ? '今日' : '累计'}总里程</div>
|
||||
<div className="text-[7px] font-bold text-slate-500 uppercase tracking-wider">{sortBy === 'today' ? (isRangeMode ? '区间' : '当日') : '累计'}总里程</div>
|
||||
<div className="text-lg font-black tracking-tighter leading-tight flex items-baseline gap-1">
|
||||
{pageLoading ? <div className="h-5 w-20 bg-slate-700 rounded animate-pulse"></div> : <>{Math.round(sortBy === 'today' ? stats.totalToday : stats.totalAll).toLocaleString()} <span className="text-[8px] text-slate-400">km</span></>}
|
||||
</div>
|
||||
<div className="mt-0.5 truncate text-[8px] font-bold text-slate-500">{rangeLabel}</div>
|
||||
</div>
|
||||
<div className="bg-white p-2.5 rounded-xl border border-gray-100 shadow-sm">
|
||||
<div className="text-[7px] font-bold text-slate-400 uppercase">平均单车</div>
|
||||
@@ -1007,6 +1129,66 @@ export default function MonitoringView() {
|
||||
<div className="text-[7px] text-slate-400">台</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="rounded-xl border border-slate-100 bg-white shadow-sm overflow-hidden">
|
||||
{pageLoading ? (
|
||||
<div className="h-[74px] bg-slate-50 animate-pulse" />
|
||||
) : isRangeMode ? (
|
||||
<div className="grid grid-cols-[92px_minmax(0,1fr)_62px] items-center gap-2 px-2 py-2">
|
||||
<div className="min-w-0">
|
||||
<div className="flex items-center gap-1 text-[10px] font-black text-slate-700">
|
||||
<CalendarDays size={12} className="text-blue-500" />
|
||||
区间走势
|
||||
</div>
|
||||
<div className="mt-1 text-[9px] font-bold text-slate-400">{rangeDailyTotals.length} 天 · km</div>
|
||||
<div className="mt-0.5 truncate text-[9px] font-bold text-slate-400">{rangeLabel}</div>
|
||||
</div>
|
||||
<div className="h-[58px] min-w-0">
|
||||
{rangeDailyTotals.length === 0 ? (
|
||||
<div className="flex h-full items-center justify-center rounded-lg bg-slate-50 text-[10px] font-bold text-slate-300">暂无趋势</div>
|
||||
) : (
|
||||
<ResponsiveContainer width="100%" height={58} minWidth={0}>
|
||||
<BarChart data={rangeDailyTotals} margin={{ top: 4, right: 2, bottom: 0, left: 2 }}>
|
||||
<Tooltip
|
||||
formatter={(value) => [`${Number(value ?? 0).toLocaleString('zh-CN', { maximumFractionDigits: 2 })} km`, '当日里程']}
|
||||
labelFormatter={(label) => `日期 ${label}`}
|
||||
contentStyle={{ borderRadius: 10, borderColor: '#e2e8f0', fontSize: 11 }}
|
||||
cursor={{ fill: 'rgba(37, 99, 235, 0.06)' }}
|
||||
/>
|
||||
{averageDailyKm > 0 && <ReferenceLine y={averageDailyKm} stroke="#f59e0b" strokeDasharray="3 3" />}
|
||||
<Bar dataKey="totalKm" radius={[3, 3, 0, 0]} fill="#38bdf8" />
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<div className="text-[9px] font-black text-slate-400">日均</div>
|
||||
<div className="text-xs font-black text-slate-800">{Math.round(averageDailyKm).toLocaleString()}</div>
|
||||
<div className="text-[9px] font-bold text-slate-400">km</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-[minmax(0,1fr)_84px_84px] items-center gap-2 px-3 py-2">
|
||||
<div className="min-w-0">
|
||||
<div className="flex items-center gap-1 text-[10px] font-black text-slate-700">
|
||||
<CalendarDays size={12} className="text-blue-500" />
|
||||
单日概览
|
||||
</div>
|
||||
<div className="mt-1 truncate text-[9px] font-bold text-slate-400">{rangeLabel} · 单位 km</div>
|
||||
<div className="mt-1 truncate text-[10px] font-bold text-slate-500">
|
||||
当前列表最高 {topLoadedVehicle ? `${topLoadedVehicle.plate} · ${Math.round(topLoadedVehicle.dailyKm).toLocaleString()} km` : '-'}
|
||||
</div>
|
||||
</div>
|
||||
<div className="rounded-lg bg-blue-50 px-2 py-1.5 text-right">
|
||||
<div className="text-[9px] font-black text-blue-400">当日总计</div>
|
||||
<div className="text-xs font-black text-blue-700">{Math.round(stats.totalToday).toLocaleString()}</div>
|
||||
</div>
|
||||
<div className="rounded-lg bg-slate-50 px-2 py-1.5 text-right">
|
||||
<div className="text-[9px] font-black text-slate-400">日均单车</div>
|
||||
<div className="text-xs font-black text-slate-800">{stats.vehicleCount > 0 ? Math.round(stats.totalToday / stats.vehicleCount).toLocaleString() : 0}</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center justify-between px-2">
|
||||
<span className="text-[9px] font-black text-slate-400 uppercase tracking-widest">车辆详情清单</span>
|
||||
<span className="text-[9px] font-bold text-slate-300">{total} 条</span>
|
||||
@@ -1072,7 +1254,7 @@ export default function MonitoringView() {
|
||||
{!v.isDataSynced && v.totalKm == null && (
|
||||
<div className="w-2 h-2 rounded-full bg-amber-400 animate-pulse" title="未对接车机数据"></div>
|
||||
)}
|
||||
<span className="text-[7px] font-black text-blue-600/40 bg-blue-50 w-3 h-3 rounded flex items-center justify-center leading-none">今</span>
|
||||
<span className="text-[7px] font-black text-blue-600/50 bg-blue-50 w-3 h-3 rounded flex items-center justify-center leading-none" title={isRangeMode ? '区间内里程' : '当日里程'}>区</span>
|
||||
<div className={`text-sm font-black leading-none ${(v.isDataSynced || v.totalKm != null) ? (highMileageAlert ? 'text-red-600' : 'text-blue-600') : 'text-amber-600'}`}>
|
||||
{(v.isDataSynced || v.totalKm != null) ? <>{Math.max(0, v.dailyKm || 0).toLocaleString()} <span className={`text-[8px] ${highMileageAlert ? 'text-red-400' : 'text-slate-400'}`}>km</span></> : <span className="text-[7px] text-amber-500/70">未对接</span>}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user