import { useEffect, useMemo, useRef, useState } from 'react'; import { AnimatePresence, motion } from 'motion/react'; import { AlertTriangle, ArrowDown, ArrowDownUp, ArrowUp, Archive, BarChart3, CalendarDays, CheckCircle2, ChevronDown, ChevronLeft, ChevronRight, CircleGauge, Info, MapPin, RefreshCw, Route, Search, TrendingDown, TrendingUp, Truck, Warehouse, X, } from 'lucide-react'; import { Bar, BarChart, Cell, CartesianGrid, Legend, Line, LineChart, ReferenceLine, ResponsiveContainer, Tooltip, XAxis, YAxis, } from 'recharts'; import Blur from '../../components/Blur'; import { ErrorState, MetricTile, SurfaceCard, } from '../../components/ui/surface'; import { fetchDailyMileageReport, type DailyMileageReport, type MileageReportGroup, type MileageReportVehicle, } from './api'; type VehicleFilter = 'ALL' | 'LOW' | 'HIGH' | 'INVENTORY'; type VehicleSortKey = 'dailyMileage' | 'sevenDayMileage' | 'completionRate'; type SortDirection = 'asc' | 'desc'; const VEHICLE_PAGE_SIZE = 20; const MIN_REPORT_LOADING_MS = 280; const VEHICLE_FILTERS: { id: VehicleFilter; label: string }[] = [ { id: 'ALL', label: '全部' }, { id: 'LOW', label: '≤100km' }, { id: 'HIGH', label: '>100km' }, { id: 'INVENTORY', label: '库存' }, ]; function shanghaiYmd(offsetDays = 0): string { const parts = new Intl.DateTimeFormat('en-CA', { timeZone: 'Asia/Shanghai', year: 'numeric', month: '2-digit', day: '2-digit', }).format(new Date()); const date = new Date(`${parts}T00:00:00Z`); date.setUTCDate(date.getUTCDate() + offsetDays); return date.toISOString().slice(0, 10); } function dateShift(date: string, days: number): string { const value = new Date(`${date}T00:00:00Z`); value.setUTCDate(value.getUTCDate() + days); return value.toISOString().slice(0, 10); } function monthShift(month: string, months: number): string { const value = new Date(`${month}-01T00:00:00Z`); value.setUTCMonth(value.getUTCMonth() + months); return value.toISOString().slice(0, 7); } function reportDateLabel(value: string): string { const date = new Date(`${value}T00:00:00Z`); return new Intl.DateTimeFormat('zh-CN', { timeZone: 'UTC', year: 'numeric', month: 'long', day: 'numeric', weekday: 'short', }).format(date).replace(/日(?=周)/, '日 '); } function initialReportDate(): string { const params = new URLSearchParams(window.location.search); const value = params.get('mileageReportDate'); return /^\d{4}-\d{2}-\d{2}$/.test(value || '') ? value! : shanghaiYmd(-1); } function ReportDatePicker({ value, maxDate, onChange, }: { value: string; maxDate: string; onChange: (date: string) => void; }) { const [open, setOpen] = useState(false); const [month, setMonth] = useState(value.slice(0, 7)); const monthStart = new Date(`${month}-01T00:00:00Z`); const firstGridDate = new Date(monthStart); firstGridDate.setUTCDate(1 - monthStart.getUTCDay()); const days = Array.from({ length: 42 }, (_, index) => { const date = new Date(firstGridDate); date.setUTCDate(firstGridDate.getUTCDate() + index); return date; }); const weekdayLabels = ['日', '一', '二', '三', '四', '五', '六']; const monthLabel = new Intl.DateTimeFormat('zh-CN', { timeZone: 'UTC', year: 'numeric', month: 'long', }).format(monthStart); return (
{open ? (
{monthLabel}
{weekdayLabels.map(day => {day})} {days.map(date => { const dateValue = date.toISOString().slice(0, 10); const inMonth = dateValue.slice(0, 7) === month; const selected = dateValue === value; const disabled = dateValue > maxDate; return ( ); })}
) : null}
); } function DailyReportLoadingOverlay({ reportDate }: { reportDate: string }) { return (
正在加载日报
正在汇总 {fmtDate(reportDate)} 的里程数据
); } function updateReportDateInUrl(date: string): void { const url = new URL(window.location.href); url.searchParams.set('mileageReportDate', date); window.history.replaceState(null, '', `${url.pathname}${url.search}${url.hash}`); } function fmtKm(value: number): string { if (Math.abs(value) >= 10000) return `${(value / 10000).toFixed(2)}万`; return value.toLocaleString('zh-CN', { maximumFractionDigits: 1 }); } function fmtDate(value: string): string { const [, month, day] = value.split('-'); return `${Number(month)}月${Number(day)}日`; } function fmtDateTime(value: string | null): string { if (!value) return '数据时间未返回'; const date = new Date(value); if (Number.isNaN(date.getTime())) return value; return date.toLocaleString('zh-CN', { month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', hour12: false, }); } function deltaLabel(value: number, rate: number | null): string { const sign = value >= 0 ? '+' : ''; return `${sign}${fmtKm(value)} km${rate == null ? '' : ` · ${sign}${rate.toFixed(1)}%`}`; } function currentAssessmentYearLabel(periods: string[], reportDate: string): string | null { const report = new Date(`${reportDate}T00:00:00Z`); if (Number.isNaN(report.getTime())) return null; const years = new Set(); for (const period of periods) { const dates = Array.from(period.matchAll(/(\d{4})[/-](\d{1,2})[/-](\d{1,2})/g)); const start = dates[0]; const end = dates[1]; if (!start || !end) continue; const startDate = new Date(Date.UTC(Number(start[1]), Number(start[2]) - 1, Number(start[3]))); const endDate = new Date(Date.UTC(Number(end[1]), Number(end[2]) - 1, Number(end[3]))); if (report < startDate || report > endDate) continue; let yearNumber = report.getUTCFullYear() - startDate.getUTCFullYear() + 1; if ( report.getUTCMonth() < startDate.getUTCMonth() || (report.getUTCMonth() === startDate.getUTCMonth() && report.getUTCDate() < startDate.getUTCDate()) ) yearNumber -= 1; if (yearNumber > 0) years.add(yearNumber); } if (years.size === 0) return null; const labels = Array.from(years).sort((a, b) => a - b).map(year => String(year)); return `考核第${labels.join('/')}年`; } function dailyMileageStatus(group: MileageReportGroup): { label: string; detail: string; tone: 'positive' | 'warning' | 'neutral'; } { if (group.dailyRequiredMileage <= 0) { if (group.remainingMileage <= 0) { return { label: '本年度已完成', detail: '当前年度剩余任务为 0,无需设置每日应完成里程。', tone: 'positive' }; } return { label: '暂无法计算', detail: '缺少有效的当前考核年度截止日,暂不设每日应完成里程。', tone: 'warning' }; } const gap = group.dailyMileage - group.dailyRequiredMileage; const rate = (group.dailyMileage / group.dailyRequiredMileage) * 100; if (gap >= 0) { return { label: '今日已达标', detail: `超出应完成 ${fmtKm(gap)} km,完成 ${rate.toFixed(1)}%。`, tone: 'positive' }; } return { label: '今日未达标', detail: `距应完成差 ${fmtKm(Math.abs(gap))} km,完成 ${rate.toFixed(1)}%。`, tone: 'warning' }; } function InsightIcon({ tone }: { tone: string }) { if (tone === 'positive') return ; if (tone === 'critical') return ; if (tone === 'warning') return ; return ; } function sevenDayMileageOf(vehicle: MileageReportVehicle): number { return (vehicle.trend || []).reduce((total, point) => total + point.totalMileage, 0); } function SortableVehicleHeader({ label, sortKey, activeSortKey, sortDirection, onSort, }: { label: string; sortKey: VehicleSortKey; activeSortKey: VehicleSortKey; sortDirection: SortDirection; onSort: (sortKey: VehicleSortKey) => void; }) { const active = activeSortKey === sortKey; const Icon = !active ? ArrowDownUp : sortDirection === 'asc' ? ArrowUp : ArrowDown; const order = active ? (sortDirection === 'asc' ? '升序' : '降序') : '未排序'; return ( ); } function MobileDailyOverview({ report, highMileageRate, }: { report: DailyMileageReport; highMileageRate: number; }) { const operatingRate = (report.totals.operatingCount / Math.max(1, report.totals.vehicleCount)) * 100; const stats = [ { label: '运营车辆', value: report.totals.operatingCount, unit: '台', detail: `${report.totals.vehicleCount} 台考核车辆 · ${operatingRate.toFixed(1)}%`, icon: Truck, tone: 'text-emerald-600 bg-emerald-50', }, { label: '库存车辆', value: report.totals.inventoryCount, unit: '台', detail: `产生 ${fmtKm(report.totals.inventoryMileage)} km`, icon: Warehouse, tone: 'text-rose-600 bg-rose-50', }, { label: '运营单车均值', value: fmtKm(report.totals.averageOperatingMileage), unit: 'km/台', detail: `运营里程 ${fmtKm(report.totals.operatingMileage)} km`, icon: CircleGauge, tone: 'text-slate-600 bg-slate-100', }, { label: '高里程车辆', value: report.totals.highMileageCount, unit: '台', detail: `${highMileageRate.toFixed(1)}% · 低里程 ${report.totals.lowMileageCount} 台`, icon: TrendingUp, tone: 'text-blue-600 bg-blue-50', }, ]; return (
当日总里程
{fmtKm(report.totals.dailyMileage)} km
= 0 ? 'bg-emerald-50' : 'bg-rose-50'}`}>
较前一日
= 0 ? 'text-emerald-600' : 'text-rose-600'}`}> {deltaLabel(report.totals.dayOverDayDelta, report.totals.dayOverDayRate)}
{stats.map((stat, index) => { const Icon = stat.icon; return (
{stat.label}
{stat.value} {stat.unit}
{stat.detail}
); })}
); } function BreakdownList({ title, icon, items, showAverage = true, }: { title: string; icon: React.ReactNode; items: MileageReportGroup['departments']; showAverage?: boolean; }) { const max = Math.max(1, ...items.map(item => item.vehicleCount)); return (
{icon} {title}
{items.length === 0 ? (
暂无数据
) : (
{items.map(item => (
{item.name} {item.vehicleCount} 台{showAverage ? ` · 均 ${fmtKm(item.averageMileage)} km` : ''}
))}
)}
); } function VehicleTable({ group }: { group: MileageReportGroup }) { const [filter, setFilter] = useState('ALL'); const [search, setSearch] = useState(''); const [page, setPage] = useState(1); const [sortKey, setSortKey] = useState('dailyMileage'); const [sortDirection, setSortDirection] = useState('desc'); const [trendPlate, setTrendPlate] = useState(null); const trendPanelRef = useRef(null); const filteredVehicles = useMemo(() => { const query = search.trim().toLowerCase(); return group.vehicles.filter(vehicle => { if (filter !== 'ALL' && vehicle.mileageBand !== filter) return false; if (!query) return true; return vehicle.plate.toLowerCase().includes(query) || (vehicle.customer || '').toLowerCase().includes(query) || (vehicle.department || '').toLowerCase().includes(query); }); }, [filter, group.vehicles, search]); const vehicles = useMemo(() => [...filteredVehicles].sort((left, right) => { const leftValue = sortKey === 'sevenDayMileage' ? sevenDayMileageOf(left) : left[sortKey]; const rightValue = sortKey === 'sevenDayMileage' ? sevenDayMileageOf(right) : right[sortKey]; const difference = leftValue - rightValue; if (difference !== 0) return sortDirection === 'asc' ? difference : -difference; return left.plate.localeCompare(right.plate, 'zh-CN'); }), [filteredVehicles, sortDirection, sortKey]); const totalPages = Math.max(1, Math.ceil(vehicles.length / VEHICLE_PAGE_SIZE)); const currentPage = Math.min(page, totalPages); const displayedVehicles = vehicles.slice( (currentPage - 1) * VEHICLE_PAGE_SIZE, currentPage * VEHICLE_PAGE_SIZE, ); const firstVehicleIndex = vehicles.length === 0 ? 0 : (currentPage - 1) * VEHICLE_PAGE_SIZE + 1; const lastVehicleIndex = Math.min(currentPage * VEHICLE_PAGE_SIZE, vehicles.length); const toggleSort = (nextSortKey: VehicleSortKey) => { setPage(1); if (nextSortKey === sortKey) { setSortDirection(current => current === 'asc' ? 'desc' : 'asc'); return; } setSortKey(nextSortKey); setSortDirection('desc'); }; const trendVehicle = trendPlate ? group.vehicles.find(vehicle => vehicle.plate === trendPlate) || null : null; const trendData = trendVehicle?.trend?.length ? trendVehicle.trend : group.trend.map((point, index) => ({ date: point.date, totalMileage: index === group.trend.length - 1 ? trendVehicle?.dailyMileage || 0 : 0, })); const sevenDayTotal = roundMileageForDisplay(trendData.reduce((total, point) => total + point.totalMileage, 0)); useEffect(() => { if (!trendPlate) return; const frame = window.requestAnimationFrame(() => { trendPanelRef.current?.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); }); return () => window.cancelAnimationFrame(frame); }, [trendPlate]); return (
{VEHICLE_FILTERS.map(item => ( ))}
{trendVehicle ? (
{trendVehicle.plate} 近7日里程
区间累计 {fmtKm(sevenDayTotal)} km 日均 {fmtKm(roundMileageForDisplay(sevenDayTotal / Math.max(1, trendData.length)))} km 当日 {fmtKm(trendVehicle.dailyMileage)} km
fmtDate(String(label))} formatter={value => [`${fmtKm(Number(value))} km`, '当日里程']} contentStyle={{ borderRadius: 8, border: '1px solid #e2e8f0', fontSize: 11 }} /> {trendData.map(point => ( 100 ? '#2563eb' : '#94a3b8'} /> ))}
) : null}
车牌/状态车牌 状态 部门/地区客户
{displayedVehicles.map(vehicle => { const sevenDayMileage = sevenDayMileageOf(vehicle); const selected = trendPlate === vehicle.plate; const frozenCellClass = selected ? '!bg-blue-50' : '!bg-white'; return (
setTrendPlate(vehicle.plate)} onKeyDown={event => { if (event.key === 'Enter' || event.key === ' ') { event.preventDefault(); setTrendPlate(vehicle.plate); } }} className={`grid cursor-pointer grid-cols-[112px_88px_minmax(140px,1fr)_82px_90px_80px_28px] items-center gap-3 px-3 py-2.5 text-[11px] font-bold outline-none transition-colors hover:bg-slate-50 focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-blue-500 md:grid-cols-[110px_72px_100px_minmax(160px,1fr)_90px_100px_88px_32px] ${selected ? 'bg-blue-50/60' : ''}`} > {vehicle.plate} {vehicle.status} {vehicle.department || vehicle.inventoryRegion || '未标注'} {vehicle.customer || '未绑定客户'} 0 ? 'text-rose-600' : 'text-slate-700'}`}> {fmtKm(vehicle.dailyMileage)} km {fmtKm(sevenDayMileage)} km {vehicle.completionRate.toFixed(1)}%
);})} {vehicles.length === 0 ?
没有符合条件的车辆
: null}
显示 {firstVehicleIndex}-{lastVehicleIndex} / {vehicles.length} 台 {totalPages > 1 ? (
{currentPage}/{totalPages}
) : null}
); } function GroupRow({ group, reportDate, expanded, onToggle, }: { group: MileageReportGroup; reportDate: string; expanded: boolean; onToggle: () => void; }) { const highRate = group.operatingCount > 0 ? (group.highMileageCount / group.operatingCount) * 100 : 0; const qualifiedRate = group.vehicleCount > 0 ? (group.qualifiedCount / group.vehicleCount) * 100 : 0; const assessmentYearLabel = currentAssessmentYearLabel(group.assessmentPeriods, reportDate); const dailyStatus = dailyMileageStatus(group); const dailyStatusClass = dailyStatus.tone === 'positive' ? 'border-emerald-100 bg-emerald-50/70 text-emerald-700' : dailyStatus.tone === 'warning' ? 'border-amber-100 bg-amber-50/70 text-amber-700' : 'border-slate-100 bg-slate-50 text-slate-600'; return (
{expanded ? (
} items={group.departments} /> } items={group.inventoryRegions} showAverage={false} />
考核进度
累计完成率
{group.completionRate.toFixed(1)}%
过半车辆
{group.halfQualifiedCount} 台
剩余任务
{fmtKm(group.remainingMileage)} km
每日应完成
{fmtKm(group.dailyRequiredMileage)} km
当日目标执行 · {dailyStatus.label}
当日实际 {fmtKm(group.dailyMileage)} km / 应完成 {fmtKm(group.dailyRequiredMileage)} km
{dailyStatus.detail}
累计达标率
已达标 {group.qualifiedCount} / {group.vehicleCount} 台
{qualifiedRate.toFixed(1)}%
{group.assessmentPeriods.join(';')}
) : null}
); } function roundMileageForDisplay(value: number): number { return Math.round(value * 10) / 10; } export default function DailyReportView() { const maxDate = shanghaiYmd(-1); const [selectedDate, setSelectedDate] = useState(initialReportDate); const [report, setReport] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(''); const [expandedTargetId, setExpandedTargetId] = useState(null); const [trendTargetId, setTrendTargetId] = useState(null); useEffect(() => { let active = true; let finishTimer: number | undefined; const loadingStartedAt = performance.now(); setLoading(true); setError(''); updateReportDateInUrl(selectedDate); fetchDailyMileageReport(selectedDate) .then(data => { if (!active) return; setReport(data); setExpandedTargetId(current => current != null && data.groups.some(group => group.targetId === current) ? current : null); setTrendTargetId(current => current != null && data.groups.some(group => group.targetId === current) ? current : null); }) .catch(cause => { if (!active) return; setError(cause instanceof Error ? cause.message : '日报加载失败'); }) .finally(() => { if (!active) return; const delay = Math.max(0, MIN_REPORT_LOADING_MS - (performance.now() - loadingStartedAt)); finishTimer = window.setTimeout(() => { if (active) setLoading(false); }, delay); }); return () => { active = false; if (finishTimer != null) window.clearTimeout(finishTimer); }; }, [selectedDate]); const highMileageRate = report && report.totals.operatingCount > 0 ? (report.totals.highMileageCount / report.totals.operatingCount) * 100 : 0; const comparisonData = report?.groups.map(group => ({ name: group.displayName, 运营里程: group.operatingMileage, 库存里程: group.inventoryMileage, })) || []; const visibleQualityNotes = report?.qualityNotes.filter( note => note.message !== '日行驶总里程延续人工汇报口径,包含库存车辆产生的里程。', ) || []; if (loading && !report) return ; if (!report) return ; const trendGroup = trendTargetId == null ? null : report.groups.find(group => group.targetId === trendTargetId) || null; const trendData = trendGroup?.trend || report.trend; const trendName = trendGroup?.displayName || '总计'; const trendSubtitle = '选择总计或车型查看近 7 日每日里程,包含库存车辆里程'; const trendColor = trendGroup ? '#0f766e' : '#2563eb'; return (

{report.title}

{report.status === 'ARCHIVED' ? : } {report.status === 'ARCHIVED' ? '已归档' : '预览'}

{fmtDate(report.reportDate)} · {report.source === 'XLSX_IMPORT' ? '人工台账归档' : report.status === 'ARCHIVED' ? `归档于 ${fmtDateTime(report.generatedAt)}` : `仪表数据更新至 ${fmtDateTime(report.sourceUpdatedAt)}`}

统计周期:自然日 · 汇总频率:每日 00:30 汇总前一日里程

{visibleQualityNotes.length > 0 ? (
{visibleQualityNotes.map(note => (
{note.level === 'warning' ? : } {note.message}
))}
) : null}
0 ? 'rose' : 'amber'} compact />
{report.insights.map(insight => { const toneClass = insight.tone === 'positive' ? 'text-emerald-600 bg-emerald-50' : insight.tone === 'critical' ? 'text-rose-600 bg-rose-50' : insight.tone === 'warning' ? 'text-amber-600 bg-amber-50' : 'text-blue-600 bg-blue-50'; return (
{insight.title}
{insight.detail}
); })}
{report.groups.map(group => ( ))}
)} >
fmtDate(String(label))} formatter={value => [`${fmtKm(Number(value))} km`, trendName]} contentStyle={{ borderRadius: 8, border: '1px solid #e2e8f0', fontSize: 11 }} />
车型里程构成 统计日期 {fmtDate(report.reportDate)} )} subtitle="运营与库存里程分开展示" >
[`${fmtKm(Number(value))} km`, String(name)]} contentStyle={{ borderRadius: 8, border: '1px solid #e2e8f0', fontSize: 11 }} />
里程分档口径
低里程:当日里程 ≤100km 高里程:当日里程 >100km 仅统计运营车辆,库存车辆单独统计
车型运营明细 统计日期 {fmtDate(report.reportDate)} )} subtitle="点击车型查看部门、库存地区、考核进度及车辆近7日趋势" >
考核车型当日里程环比变化低/高里程高里程占比年度达标
{report.groups.map(group => ( setExpandedTargetId(current => current === group.targetId ? null : group.targetId)} /> ))}
{loading ? : null} {error ? : null}
); }