This commit is contained in:
@@ -41,7 +41,6 @@ import {
|
|||||||
import Blur from '../../components/Blur';
|
import Blur from '../../components/Blur';
|
||||||
import {
|
import {
|
||||||
ErrorState,
|
ErrorState,
|
||||||
LoadingState,
|
|
||||||
MetricTile,
|
MetricTile,
|
||||||
SurfaceCard,
|
SurfaceCard,
|
||||||
} from '../../components/ui/surface';
|
} from '../../components/ui/surface';
|
||||||
@@ -57,6 +56,7 @@ type VehicleSortKey = 'dailyMileage' | 'sevenDayMileage' | 'completionRate';
|
|||||||
type SortDirection = 'asc' | 'desc';
|
type SortDirection = 'asc' | 'desc';
|
||||||
|
|
||||||
const VEHICLE_PAGE_SIZE = 20;
|
const VEHICLE_PAGE_SIZE = 20;
|
||||||
|
const MIN_REPORT_LOADING_MS = 280;
|
||||||
|
|
||||||
const VEHICLE_FILTERS: { id: VehicleFilter; label: string }[] = [
|
const VEHICLE_FILTERS: { id: VehicleFilter; label: string }[] = [
|
||||||
{ id: 'ALL', label: '全部' },
|
{ 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 {
|
function updateReportDateInUrl(date: string): void {
|
||||||
const url = new URL(window.location.href);
|
const url = new URL(window.location.href);
|
||||||
url.searchParams.set('mileageReportDate', date);
|
url.searchParams.set('mileageReportDate', date);
|
||||||
@@ -831,7 +873,7 @@ function roundMileageForDisplay(value: number): number {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function DailyReportView() {
|
export default function DailyReportView() {
|
||||||
const maxDate = shanghaiYmd();
|
const maxDate = shanghaiYmd(-1);
|
||||||
const [selectedDate, setSelectedDate] = useState(initialReportDate);
|
const [selectedDate, setSelectedDate] = useState(initialReportDate);
|
||||||
const [report, setReport] = useState<DailyMileageReport | null>(null);
|
const [report, setReport] = useState<DailyMileageReport | null>(null);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
@@ -841,6 +883,8 @@ export default function DailyReportView() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let active = true;
|
let active = true;
|
||||||
|
let finishTimer: number | undefined;
|
||||||
|
const loadingStartedAt = performance.now();
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError('');
|
setError('');
|
||||||
updateReportDateInUrl(selectedDate);
|
updateReportDateInUrl(selectedDate);
|
||||||
@@ -855,8 +899,17 @@ export default function DailyReportView() {
|
|||||||
if (!active) return;
|
if (!active) return;
|
||||||
setError(cause instanceof Error ? cause.message : '日报加载失败');
|
setError(cause instanceof Error ? cause.message : '日报加载失败');
|
||||||
})
|
})
|
||||||
.finally(() => { if (active) setLoading(false); });
|
.finally(() => {
|
||||||
return () => { active = false; };
|
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]);
|
}, [selectedDate]);
|
||||||
|
|
||||||
const highMileageRate = report && report.totals.operatingCount > 0
|
const highMileageRate = report && report.totals.operatingCount > 0
|
||||||
@@ -871,7 +924,7 @@ export default function DailyReportView() {
|
|||||||
note => note.message !== '日行驶总里程延续人工汇报口径,包含库存车辆产生的里程。',
|
note => note.message !== '日行驶总里程延续人工汇报口径,包含库存车辆产生的里程。',
|
||||||
) || [];
|
) || [];
|
||||||
|
|
||||||
if (loading && !report) return <LoadingState label="正在生成里程日报" />;
|
if (loading && !report) return <DailyReportLoadingOverlay reportDate={selectedDate} />;
|
||||||
if (!report) return <ErrorState message={error || '日报接口未返回有效数据'} />;
|
if (!report) return <ErrorState message={error || '日报接口未返回有效数据'} />;
|
||||||
|
|
||||||
const trendGroup = trendTargetId == null ? null : report.groups.find(group => group.targetId === trendTargetId) || null;
|
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';
|
const trendColor = trendGroup ? '#0f766e' : '#2563eb';
|
||||||
|
|
||||||
return (
|
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">
|
<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="flex flex-col gap-3 lg:flex-row lg:items-center lg:justify-between">
|
||||||
<div className="min-w-0">
|
<div className="min-w-0">
|
||||||
@@ -917,15 +970,6 @@ export default function DailyReportView() {
|
|||||||
<ChevronLeft size={16} />
|
<ChevronLeft size={16} />
|
||||||
</button>
|
</button>
|
||||||
<ReportDatePicker value={selectedDate} maxDate={maxDate} onChange={setSelectedDate} />
|
<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
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setSelectedDate(dateShift(selectedDate, 1))}
|
onClick={() => setSelectedDate(dateShift(selectedDate, 1))}
|
||||||
@@ -1139,11 +1183,9 @@ export default function DailyReportView() {
|
|||||||
))}
|
))}
|
||||||
</SurfaceCard>
|
</SurfaceCard>
|
||||||
|
|
||||||
{loading ? (
|
<AnimatePresence initial={false}>
|
||||||
<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">
|
{loading ? <DailyReportLoadingOverlay reportDate={selectedDate} /> : null}
|
||||||
<RefreshCw size={14} className="animate-spin" />正在切换日报
|
</AnimatePresence>
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
{error ? <ErrorState message={error} /> : null}
|
{error ? <ErrorState message={error} /> : null}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user