This commit is contained in:
@@ -41,7 +41,6 @@ import {
|
||||
import Blur from '../../components/Blur';
|
||||
import {
|
||||
ErrorState,
|
||||
LoadingState,
|
||||
MetricTile,
|
||||
SurfaceCard,
|
||||
} from '../../components/ui/surface';
|
||||
@@ -57,6 +56,7 @@ 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: '全部' },
|
||||
@@ -209,6 +209,48 @@ function ReportDatePicker({
|
||||
);
|
||||
}
|
||||
|
||||
function DailyReportLoadingOverlay({ reportDate }: { reportDate: string }) {
|
||||
return (
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{ duration: 0.16, ease: 'easeOut' }}
|
||||
role="status"
|
||||
aria-live="polite"
|
||||
className="fixed inset-0 z-[100] flex items-center justify-center bg-slate-950/20 p-5 backdrop-blur-[2px]"
|
||||
>
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 12, scale: 0.96 }}
|
||||
animate={{ opacity: 1, y: 0, scale: 1 }}
|
||||
exit={{ opacity: 0, y: 8, scale: 0.98 }}
|
||||
transition={{ duration: 0.22, ease: 'easeOut' }}
|
||||
className="w-full max-w-[260px] overflow-hidden rounded-xl border border-slate-100 bg-white p-5 text-center shadow-2xl"
|
||||
>
|
||||
<div className="relative mx-auto flex h-12 w-12 items-center justify-center">
|
||||
<motion.span
|
||||
className="absolute inset-0 rounded-full border-2 border-blue-100"
|
||||
animate={{ scale: [0.86, 1.15], opacity: [0.9, 0] }}
|
||||
transition={{ duration: 1.2, repeat: Infinity, ease: 'easeOut' }}
|
||||
/>
|
||||
<span className="relative flex h-10 w-10 items-center justify-center rounded-full bg-blue-50 text-blue-600">
|
||||
<RefreshCw size={19} className="animate-spin" />
|
||||
</span>
|
||||
</div>
|
||||
<div className="mt-3 text-sm font-black text-slate-900">正在加载日报</div>
|
||||
<div className="mt-1 text-[11px] font-bold text-slate-400">正在汇总 {fmtDate(reportDate)} 的里程数据</div>
|
||||
<div className="mt-4 h-1 overflow-hidden rounded-full bg-slate-100">
|
||||
<motion.div
|
||||
className="h-full w-1/2 rounded-full bg-blue-500"
|
||||
animate={{ x: ['-100%', '220%'] }}
|
||||
transition={{ duration: 1.15, repeat: Infinity, ease: 'easeInOut' }}
|
||||
/>
|
||||
</div>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
|
||||
function updateReportDateInUrl(date: string): void {
|
||||
const url = new URL(window.location.href);
|
||||
url.searchParams.set('mileageReportDate', date);
|
||||
@@ -831,7 +873,7 @@ function roundMileageForDisplay(value: number): number {
|
||||
}
|
||||
|
||||
export default function DailyReportView() {
|
||||
const maxDate = shanghaiYmd();
|
||||
const maxDate = shanghaiYmd(-1);
|
||||
const [selectedDate, setSelectedDate] = useState(initialReportDate);
|
||||
const [report, setReport] = useState<DailyMileageReport | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
@@ -841,6 +883,8 @@ export default function DailyReportView() {
|
||||
|
||||
useEffect(() => {
|
||||
let active = true;
|
||||
let finishTimer: number | undefined;
|
||||
const loadingStartedAt = performance.now();
|
||||
setLoading(true);
|
||||
setError('');
|
||||
updateReportDateInUrl(selectedDate);
|
||||
@@ -855,8 +899,17 @@ export default function DailyReportView() {
|
||||
if (!active) return;
|
||||
setError(cause instanceof Error ? cause.message : '日报加载失败');
|
||||
})
|
||||
.finally(() => { if (active) setLoading(false); });
|
||||
return () => { active = false; };
|
||||
.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
|
||||
@@ -871,7 +924,7 @@ export default function DailyReportView() {
|
||||
note => note.message !== '日行驶总里程延续人工汇报口径,包含库存车辆产生的里程。',
|
||||
) || [];
|
||||
|
||||
if (loading && !report) return <LoadingState label="正在生成里程日报" />;
|
||||
if (loading && !report) return <DailyReportLoadingOverlay reportDate={selectedDate} />;
|
||||
if (!report) return <ErrorState message={error || '日报接口未返回有效数据'} />;
|
||||
|
||||
const trendGroup = trendTargetId == null ? null : report.groups.find(group => group.targetId === trendTargetId) || null;
|
||||
@@ -881,7 +934,7 @@ export default function DailyReportView() {
|
||||
const trendColor = trendGroup ? '#0f766e' : '#2563eb';
|
||||
|
||||
return (
|
||||
<div className="space-y-3 pb-5">
|
||||
<div className="relative space-y-3 pb-5" aria-busy={loading}>
|
||||
<section className="rounded-xl border border-slate-200 bg-white px-4 py-3 shadow-sm">
|
||||
<div className="flex flex-col gap-3 lg:flex-row lg:items-center lg:justify-between">
|
||||
<div className="min-w-0">
|
||||
@@ -917,15 +970,6 @@ export default function DailyReportView() {
|
||||
<ChevronLeft size={16} />
|
||||
</button>
|
||||
<ReportDatePicker value={selectedDate} maxDate={maxDate} onChange={setSelectedDate} />
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setSelectedDate(maxDate)}
|
||||
disabled={selectedDate === maxDate}
|
||||
className="flex h-9 shrink-0 items-center justify-center rounded-lg border border-slate-200 bg-white px-2.5 text-[11px] font-black text-blue-600 hover:border-blue-200 hover:bg-blue-50 disabled:cursor-not-allowed disabled:opacity-35"
|
||||
title="回到今天"
|
||||
>
|
||||
今天
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setSelectedDate(dateShift(selectedDate, 1))}
|
||||
@@ -1139,11 +1183,9 @@ export default function DailyReportView() {
|
||||
))}
|
||||
</SurfaceCard>
|
||||
|
||||
{loading ? (
|
||||
<div className="fixed bottom-5 right-5 z-50 flex items-center gap-2 rounded-lg bg-slate-900 px-3 py-2 text-xs font-black text-white shadow-xl">
|
||||
<RefreshCw size={14} className="animate-spin" />正在切换日报
|
||||
</div>
|
||||
) : null}
|
||||
<AnimatePresence initial={false}>
|
||||
{loading ? <DailyReportLoadingOverlay reportDate={selectedDate} /> : null}
|
||||
</AnimatePresence>
|
||||
{error ? <ErrorState message={error} /> : null}
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user